Finance-QuoteHist-1.32/0000755000175000017500000000000014021707227013433 5ustar sisksiskFinance-QuoteHist-1.32/Changes0000644000175000017500000001760014021706752014734 0ustar sisksiskRevision history for Perl extension Finance::QuoteHist. 1.32 Tue 09 Mar 2021 10:01:14 AM EST - patched to handle negative epochs 1.30 Fri 19 Feb 2021 01:47:26 PM EST - Updated yahoo module to use json API 1.29 Wed Nov 13 10:27:10 EST 2019 - Fixed (harmless) dependency on google module 1.28 Wed Oct 23 12:11:32 EDT 2019 - Updated yahoo test data again - Retired Finance::QuoteHist::Google (no data) - Retired Finance::QuoteHist::DailyFinance (no data) 1.27 Tue Jul 18 10:46:49 EDT 2017 - Updated yahoo test data 1.26 Tue Jun 27 14:19:13 EDT 2017 - Fixed Yahoo module to handle the site's obfuscation - updated test data - removed defunct StockNod 1.24 Mon Aug 3 15:55:29 EDT 2015 - removed some deprecated Date::Manip config statements - removed outmoded fetchers 1.23 Thu May 21 11:48:11 EDT 2015 - fixed divident/split entanglement for yahoo - patched quotes test to properly skip tests without network connectivity (RT #104358) 1.22 Mon Apr 20 14:15:32 EDT 2015 - fixed dividend/split entanglement on yahoo rt.cpan.org #103759 1.21 Fri Jan 9 11:25:10 EST 2015 - fixed splits on yahoo - removed BusinessWeek 1.20 Tue Oct 8 14:54:48 EDT 2013 - added DailyFinance - removed QuoteMedia, MSN, Investopedia 1.19 Mon Aug 29 11:08:08 EDT 2011 - fixed test data window (1.17 was a little *too* recent for reliable monthly tests) - 1.18 skipped, CPAN version appeasement 1.17 Fri Aug 26 19:31:05 EDT 2011 - Updated tests with more recent data 1.16 Thu Sep 30 01:00:27 EDT 2010 - Fixed tests so that they no longer rely upon historical adjusted quotes since these appear to change every now and then whenever Yahoo decides to tweak their algorithm. - typo fix 1.14 Mon Jun 7 18:36:30 EDT 2010 - Fixed several syntax issues that were tripping up -w - Fixed the utf8 Byte Order Mark issues with google csv - Added modules for Investopedia.com and StockNod.com - Expanded site-specific tests (for development mostly) - Test data updated, docs refreshed and tweaked 1.12 Mon Aug 3 18:40:15 EDT 2009 - Fixed various site-specific modules - F::Q::Yahoo_AU is deprecated in favor of F::Q::Yahoo::Australia (though the old package is still around for backwards compat) - moved CSV class discovery to package level (thanks to Edmond Shum) - fixed the 'unsigned integer' problem with volumes (thanks to 'o kite') - fixed some undef warnings and typos (thanks to Jim Miner) 1.11 Mon Jun 25 19:21:21 EDT 2007 - Added Google and Yahoo_AU - Rewrote BusinessWeek, decodes from javascript channels now, since they changed how their site operates. No more intraday quotes though. 1.10 Tue Mar 13 13:57:07 EDT 2007 - Tweaked Yahoo test data yet again (they seem to be rounding off their volumes now) - Fixed undefined error for symbols with no closing (thanks to Terry Gliedt) - Added BusinessWeek to lineup, including intraday functionality. - Flattened champion delegation. - Added labels() documentation - Implemented user-defined row filtering callback (thanks to Manoj Bhatti for the suggestion) - Added 'shares' as possible label for 'vol' (thanks again to Manoj) 1.09 Thu Jun 8 16:19:57 EDT 2006 - Fixed ^DJI test data (again) since yahoo keeps changing how they represent volume (thanks to Ivo Welch) 1.08 Sat May 6 15:16:11 EDT 2006 - Fixed ^DJI test data for quote tests (Yahoo switched to the volume for each anchor date rather than aggregate volumes) - splits() now returns ref properly in scalar context for F::QH::Yahoo. Thanks to Raymond de Leon for spotting. 1.07 Thu Mar 16 00:44:24 EST 2006 - Internal restructure to date iterators where appropriate. - Doc fix for MSN. - Various minor edge cases fixed. 1.06 Fri Mar 10 15:59:05 EST 2006 - Added MSN quote source. - Fixed some edge cases involving null-queries on splits. - Various minor fixes, including proper business-day calculations for end of month dates on montly resolutions, plus proper newline stripping (chomp didn't work on MSN csv). 1.05 Tue Feb 28 16:47:57 EST 2006 - Splits now have no dependencies on dividend queries - Daily, Weekly, Monthly granularities available - Restored quote_source(), dividend_source(), and split_source() methods - More tests added 1.04 Fri Jan 6 12:18:59 EST 2006 - Compensated for yahoo symbol switch from ^DJA to ^DJI, which was causing some tests to fail. - Updated quotemedia url. 1.03 Wed Nov 2 16:52:30 EST 2005 - Minor bug fixes - Forced date calcs to GMT to avoid some cygwin/win errors - Added auto_proxy, enabled during tests so that if $ENV{HTTP_PROXY} is present $ua->env_proxy will be automatically invoked - Tests will skip if network connectivity cannot be established 1.02 Wed Aug 3 14:46:43 EDT 2005 - Fixed Date::Manip requirement that was inadvertently dropped. 1.01 Thu Jul 21 13:29:54 EDT 2005 - Added conditional support for Text::CSV_PP in cases where Text::CSV_XS is not installed - Split out tests 1.00 Thu Jun 23 16:25:53 EDT 2005 - Reworked Yahoo! queries for new interface - Added quotes from QuoteMedia (they supply SiliconInvestor, so that was dropped) - Date constraints relaxed. No end date defaults to 'today', no start date defaults to inception. Having neither date grabs entire history. ! Internal API changed from symbolic dereferencing to supplying closures and iterators for particular targets and parse modes. 0.33 - Broke up Yahoo! csv queries into blocks of 1000, as apparently Yahoo! sometimes puts a cap on these sort of queries (thanks to Jay Strauss) 0.32 Thu Aug 14 13:37:36 EDT 2003 - Fixed csv parsing for Yahoo dividend data after they fixed their format - Updated test data 0.31 Thu Jan 16 19:20:31 GMT 2003 - Rolled back some 5.6 specific syntax 0.30 Fri Nov 8 16:47:07 EST 2002 - adjusted volume fix (column labeling snafu) - extraneous status messages fixed for split extrations - added filtered split and filtered dividend tests 0.29 Thu Oct 17 04:36:05 EDT 2002 - Patched Yahoo URL interface since they switched to January = 00 rather than 01. 0.28 Thu Feb 28 08:23:08 CST 2002 - Updated Yahoo interface, incorporating new changes. - added proxy_env parameter and method to ease proxy handling via LWP::UserAgent - Added WallStreetCity.pm (basic source) - Removed FinancialWeb.pm (apparently defunct) - Removed MotleyFool.pm (no longer offers tabular data, plus login now required for historical info) 0.25 Mon Jan 8 19:35:00 CST 2001 - Doc cleanup 0.24 Wed Nov 29 02:28:59 CST 2000 - Replaced FinancialWeb with SiliconInvestor in order to provide information on defunct ticker symbols -- FinancialWeb ceased coverage of these symbols. 0.23 Thu Nov 2 15:19:00 CST 2000 - Fixed cache bug with Yahoo::source_type() - Documentation revisions and fixes 0.21 Wed Sep 13 13:25:35 CDT 2000 - Splits and dividends added - Much faster queries, primarily due to taking advantage of Yahoo and CSV-formatted data, when available - Supports grabbing non-adjusted data, as well as notification when transitioning to a site that cannot provide non-adjusted data - Supports auto-adjusting of non-adjusted quotes for tables that include an adjusted column (like Yahoo in HTML mode) - Information pedigree support -- i.e., from which site did the quote/split/dividend data come for a particular ticker symbol - Revised interface, although the old one is mostly still supported (read the docs!) - Defunct ticker symbols still supported (quotes only, no split/dividend) 0.01 Wed Feb 2 18:24:07 CST 2000 - Initial version. Finance-QuoteHist-1.32/lib/0000755000175000017500000000000014021707227014201 5ustar sisksiskFinance-QuoteHist-1.32/lib/Finance/0000755000175000017500000000000014021707227015544 5ustar sisksiskFinance-QuoteHist-1.32/lib/Finance/QuoteHist/0000755000175000017500000000000014021707227017471 5ustar sisksiskFinance-QuoteHist-1.32/lib/Finance/QuoteHist/Yahoo.pm0000644000175000017500000002731514014003605021105 0ustar sisksiskpackage Finance::QuoteHist::Yahoo; use strict; use vars qw(@ISA $VERSION); use Carp; $VERSION = "1.26"; use Finance::QuoteHist::Generic; @ISA = qw(Finance::QuoteHist::Generic); use Date::Manip; use JSON; # 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' \ # -H 'authority: query2.finance.yahoo.com' \ # -H 'pragma: no-cache' \ # -H 'cache-control: no-cache' \ # -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36' \ # -H 'dnt: 1' \ # -H 'accept: */*' \ # -H 'origin: https://finance.yahoo.com' \ # -H 'sec-fetch-site: same-site' \ # -H 'sec-fetch-mode: cors' \ # -H 'sec-fetch-dest: empty' \ # -H 'referer: https://finance.yahoo.com/quote/TLSA/history?period1=1455840000&period2=1613692800&interval=1d&filter=history&frequency=1d&includeAdjustedClose=true' \ # -H 'accept-language: en-US,en;q=0.9' \ # -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' \ # -H 'sec-gpc: 1' \ # --compressed # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=1495391410&period2=1498069810&interval=1d&events=history&crumb=bB6k340lPXt # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1wk&events=history&crumb=bB6k340lPXt # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1mo&events=history&crumb=bB6k340lPXt # # Dividends: # https://query1.finance.yahoo.com/v7/finance/download/IBM?period1=993096000&period2=1498017600&interval=1d&events=div&crumb=bB6k340lPXt # # Splits: # https://query1.finance.yahoo.com/v7/finance/download/NKE?period1=993096000&period2=1498017600&interval=1d&events=split&crumb=bB6k340lPXt sub new { my $that = shift; my $class = ref($that) || $that; my %parms = @_; $parms{parse_mode} = 'json'; $parms{ua_params} ||= {}; $parms{ua_params}{cookie_jar} ||= {}; my $self = __PACKAGE__->SUPER::new(%parms); bless $self, $class; # set initial cookie (the cookie crumbs are hashed out of this) # https://finance.yahoo.com/quote/IBM/history my $ticker = $parms{symbols}; $ticker = $ticker->[0] if ref $ticker eq 'ARRAY'; my $html = $self->fetch("https://finance.yahoo.com/quote/$ticker/history"); # extract the cookie crumb my %crumbs; for my $c ($html =~ /"crumb"\s*:\s*"([^"]+)"/g) { next if $c =~ /[{}]/; $c =~ s/\\u002F/\//; ++$crumbs{$c}; } my $crumb = ''; my $max = 0; for my $c (keys %crumbs) { if ($crumbs{$c} >= $max) { $crumb = $c; $max = $crumbs{$c}; } } $self->{crumb} = $crumb; $self; } sub granularities { qw( daily weekly monthly ) } sub url_maker { my($self, %parms) = @_; my $target_mode = $parms{target_mode} || $self->target_mode; my $parse_mode = $parms{parse_mode} || $self->parse_mode; # *always* block unknown target mode and parse mode combinations for # cascade to work properly! return undef unless $target_mode eq 'quote' || $target_mode eq 'split' || $target_mode eq 'dividend'; $parse_mode = "json"; my $granularity = lc($parms{granularity} || $self->granularity); my $grain = 'd'; $granularity =~ /^\s*(\w)/; $grain = $1 if $1 eq 'w' || $1 eq 'm'; my($ticker, $start_date, $end_date) = @parms{qw(symbol start_date end_date)}; $start_date ||= $self->start_date; $end_date ||= $self->end_date; if ($start_date && $end_date && $start_date gt $end_date) { ($start_date, $end_date) = ($end_date, $start_date); } #my $host = "query1.finance.yahoo.com"; #my $base_url = "https://$host/v7/finance/download/$ticker?"; # https://query2.finance.yahoo.com/v8/finance/chart/TLSA? # formatted=true # crumb=l92p7dftYe%2F # lang=en-US # region=US # includeAdjustedClose=true # interval=1d # period1=1455840000 # period2=1613692800 # events=div%7Csplit # useYfid=true # corsDomain=finance.yahoo.com my $host = "query2.finance.yahoo.com"; my $base_url = "https://$host/v8/finance/chart/$ticker?"; my @base_parms; if ($start_date) { my($y, $m, $d) = $self->ymd($start_date); my $ts = Date_SecsSince1970($m, $d, $y, 0, 0, 0); push(@base_parms, "period1=$ts"); } if ($end_date) { my($y, $m, $d) = $self->ymd($end_date); my $ts = Date_SecsSince1970($m, $d, $y, 0, 0, 0); $ts += 24*60*60; push(@base_parms, "period2=$ts"); } my $interval = "1d"; if ($grain eq 'w') { $interval = "1wk"; } elsif ($grain eq 'm') { $interval = "1mo"; } push(@base_parms, "interval=$interval"); if ($target_mode eq "quote") { push(@base_parms, "events=history"); push(@base_parms, "includeAdjustedClose=true") } elsif ($target_mode eq "dividend") { push(@base_parms, "events=div"); } elsif ($target_mode eq "split") { push(@base_parms, "events=split"); } push(@base_parms, "crumb=" . $self->{crumb}); my @urls = $base_url . join('&', @base_parms); return sub { pop @urls }; } sub json_parser { my $self = shift; my $target_mode = $self->target_mode(); my $json_quote_parse = sub { my $data_result = shift; my $data_indicators = $data_result->{indicators} || {}; my $data_quote = ($data_indicators->{quote} || [])->[0]; my @rows = []; if ($data_quote) { my $data_high = $data_quote->{high}; my $data_close = $data_quote->{close}; my $data_open = $data_quote->{open}; my $data_low = $data_quote->{low}; my $data_volume = $data_quote->{volume}; my $data_adj_close = $data_indicators->{adjclose}[0]{adjclose}; my $data_timestamp = $data_result->{timestamp}; for my $i (0 .. $#{$data_timestamp}) { push(@rows, [ $data_timestamp->[$i], $data_open->[$i], $data_high->[$i], $data_low->[$i], $data_close->[$i], $data_volume->[$i], ]); } } \@rows; }; my $json_split_parse = sub { my $data_result = shift; my $data_events = $data_result->{events} || {}; my $data_splits = $data_events->{splits} || {}; my @rows; if ($data_splits) { for my $rec (sort values %$data_splits) { push(@rows, [ $rec->{date}, $rec->{numerator}, $rec->{denominator}, ]); } } \@rows; }; my $json_div_parse = sub { # "date" "amount" my $data_result = shift; my $data_events = $data_result->{events} || {}; my $data_dividends = $data_events->{dividends} || {}; my @rows; for my $rec (sort values %$data_dividends) { push(@rows, [ $rec->{date}, $rec->{amount}, ]); } \@rows; }; sub { my $data = shift; $data = decode_json($data); my $data_result = $data->{chart}{result}[0] || {}; if ($target_mode eq "quote") { return $json_quote_parse->($data_result); } elsif ($target_mode eq "split") { return $json_split_parse->($data_result); } elsif ($target_mode eq "dividend") { return $json_div_parse->($data_result); } else { die "unknown mode: $target_mode" } }; } 1; __END__ =head1 NAME Finance::QuoteHist::Yahoo - Site-specific subclass for retrieving historical stock quotes. =head1 SYNOPSIS use Finance::QuoteHist::Yahoo; $q = new Finance::QuoteHist::Yahoo ( symbols => [qw(IBM UPS AMZN)], start_date => '01/01/2009', end_date => 'today', ); # Values foreach $row ($q->quotes()) { ($symbol, $date, $open, $high, $low, $close, $volume) = @$row; ... } # Splits foreach $row ($q->splits()) { ($symbol, $date, $post, $pre) = @$row; } # Dividends foreach $row ($q->dividends()) { ($symbol, $date, $dividend) = @$row; } =head1 DESCRIPTION Finance::QuoteHist::Yahoo is a subclass of Finance::QuoteHist::Generic, specifically tailored to read historical quotes, dividends, and splits from the Yahoo web site (I). For quotes and dividends, Yahoo can return data quickly in CSV format. Both of these can also be extracted from HTML tables. Splits are extracted from the HTML of the 'Basic Chart' page for that ticker. There are no date range restrictions on CSV queries for quotes and dividends. For HTML queries, Yahoo takes arbitrary date ranges as arguments, but breaks results into pages of 66 entries. Please see L for more details on usage and available methods. If you just want to get historical quotes and are not interested in the details of how it is done, check out L. =head1 METHODS The basic user interface consists of three methods, as seen in the example above. Those methods are: =over =item quotes() Returns a list of rows (or a reference to an array containing those rows, if in scalar context). Each row contains the B, B, B, B, B, B, and B for that date. =item dividends() Returns a list of rows (or a reference to an array containing those rows, if in scalar context). Each row contains the B, B, and amount of the B, in that order. =item splits() Returns a list of rows (or a reference to an array containing those rows, if in scalar context). Each row contains the B, B, B split shares, and B
 split shares, in that order.

=back

The following methods override methods provided by the
Finance::QuoteHist::Generic module; more of this was necessary than is
typical for a basic query site due to the variety of query types and
data formats available on Yahoo.

=over

=item url_maker()

Returns a subroutine reference tailored for the current target mode and
parsing mode. The routine is an iterator that will produce all necessary
URLs on repeated invocations necessary to complete a query.

=item extractors()

Returns a hash of subroutine references that attempt to extract embedded
values (dividends or splits) within the results from a larger query.

=item labels()

Includes the 'adj' column.

=back

=head1 REQUIRES

Finance::QuoteHist::Generic

=head1 DISCLAIMER

The data returned from these modules is in no way guaranteed, nor are
the developers responsible in any way for how this data (or lack
thereof) is used. The interface is based on URLs and page layouts that
might change at any time. Even though these modules are designed to be
adaptive under these circumstances, they will at some point probably
be unable to retrieve data unless fixed or provided with new
parameters. Furthermore, the data from these web sites is usually not
even guaranteed by the web sites themselves, and oftentimes is
acquired elsewhere.

If you would like to know more, check out the terms of service from
Yahoo!, which can be found here:

  http://docs.yahoo.com/info/terms/

If you still have concerns, then use another site-specific historical
quote instance, or none at all.

Above all, play nice.

=head1 AUTHOR

Matthew P. Sisk, EFE

=head1 COPYRIGHT

Copyright (c) 2000-2021 Matthew P. Sisk. All rights reserved. All wrongs
revenged. This program is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.

=head1 SEE ALSO

Finance::QuoteHist::Generic(3), Finance::QuoteHist(3), perl(1).

=cut
Finance-QuoteHist-1.32/lib/Finance/QuoteHist/Generic.pm0000644000175000017500000013764514021706005021414 0ustar  sisksiskpackage Finance::QuoteHist::Generic;

# http://www.stanford.edu/dept/OOD/RESEARCH/top-ten-faq/how_do_i_find_an_historical_st.html
#
# Shortcut: Use adjusted close price
#
# For the mathematically inclined, one shortcut to determining the value
# after splits is to use the adjusted close price from the historical
# quote tool. For June 2, 1997, it lists a market close price of 33.13
# and an adjusted close price of 1.38. Divide 33.13 by 1.38 and you come
# up with 24.007. Multiply by 1,000 and you come pretty close to the
# 24,000 share figure determined above. Or you could divide 1.38 by
# 33.13, which gives you 0.041654. Divide $33,130 by 0.041654, and you
# get $795K, which is very close to the $808K figure above.

use strict;
use Carp;

use vars qw($VERSION);
$VERSION = "1.22";

use LWP::UserAgent;
use HTTP::Request;
use Date::Manip;

my $CSV_XS_Class = 'Text::CSV_XS';
my $CSV_PP_Class = 'Text::CSV_PP';
my $CSV_Class = $CSV_XS_Class;
eval "use $CSV_Class";
if ($@) {
  $CSV_Class = $CSV_PP_Class;
  eval "use $CSV_Class";
  croak "Could not load either $CSV_XS_Class or $CSV_PP_Class : $@\n" if $@;
}

my $HTE_CLASS;
my $HTE_Class = 'HTML::TableExtract';
sub HTML_CLASS {
  if (!$HTE_CLASS) {
    eval "use $HTE_Class";
    croak $@ if $@;
    $HTE_CLASS = $HTE_Class;
  }
  $HTE_CLASS;
}

my $Default_Target_Mode = 'quote';
my $Default_Parse_Mode  = 'html';
my $Default_Granularity = 'daily';
my $Default_Vol_Pat = qr(vol|shares)i;

my %Default_Labels;
$Default_Labels{quote}{$Default_Parse_Mode} =
  [qw( date open high low close ), $Default_Vol_Pat];
$Default_Labels{dividend}{$Default_Parse_Mode} =
  [qw( date div )];
$Default_Labels{'split'}{$Default_Parse_Mode} =
  [qw( date post pre )];
$Default_Labels{intraday}{$Default_Parse_Mode} =
  [qw( date time high low close ), $Default_Vol_Pat];

my @Scalar_Flags = qw(
  verbose
  quiet
  zthresh
  quote_precision
  attempts
  adjusted
  has_non_adjusted
  env_proxy
  debug
  parse_mode
  target_mode
  granularity
  auto_proxy
  row_filter
  ua_params
);
my $SF_pat = join('|', @Scalar_Flags);

my @Array_Flags = qw(
  symbols
  lineup
);
my $AF_pat = join('|', @Array_Flags);

my @Hash_Flags = qw( ua_params );
my $HF_pat = join('|', @Hash_Flags);

sub new {
  my $that  = shift;
  my $class = ref($that) || $that;
  my(%parms, $k, $v);
  while (($k,$v) = splice(@_, 0, 2)) {
    if ($k eq 'start_date' || $k eq 'end_date' && $v !~ /^\s*$/) {
      $parms{$k} = __PACKAGE__->date_standardize($v);
    }
    elsif ($k =~ /^$AF_pat$/o) {
      if (UNIVERSAL::isa($v, 'ARRAY')) {
        $parms{$k} = $v;
      }
      elsif (ref $v) {
        croak "$k must be passed as an array ref or single-entry string\n";
      }
      else {
        $parms{$k} = [$v];
      }
    }
    elsif ($k =~ /^$HF_pat$/o) {
      if (UNIVERSAL::isa($v, 'HASH')) {
        $parms{$k} = $v;
      }
      else {
        croak "$k must be passed as a hash ref\n";
      }
    }
    elsif ($k eq 'row_filter') {
      croak "$k must be sub ref\n" unless UNIVERSAL::isa($v, 'CODE');
      $parms{$k} = $v;
    }
    elsif ($k =~ /^$SF_pat$/o) {
      $parms{$k} = $v;
    }
  }
  $parms{end_date} ||= __PACKAGE__->date_standardize('today');
  $parms{symbols} or croak "Symbol list required\n";

  my $start_date = delete $parms{start_date};
  my $end_date   = delete $parms{end_date};
  my $symbols    = delete $parms{symbols};

  # Defaults
  $parms{zthresh}          = 30 unless $parms{zthresh};
  $parms{attempts}         = 3  unless $parms{attempts};
  $parms{adjusted}         = 1  unless exists  $parms{adjusted};
  $parms{has_non_adjusted} = 0  unless defined $parms{has_non_adjusted};
  $parms{quote_precision}  = 4  unless defined $parms{quote_precision};
  $parms{auto_proxy}       = 1  unless exists  $parms{auto_proxy};
  $parms{debug}            = 0  unless defined $parms{debug};

  my $self = \%parms;
  bless $self, $class;

  my $ua_params = $parms{ua_params} || {};
  if ($parms{env_proxy}) {
    $ua_params->{env_proxy} = 1;
  }
  elsif ($parms{auto_proxy}) {
    $ua_params->{env_proxy} = 1 if $ENV{http_proxy};
  }
  $self->{ua} ||= LWP::UserAgent->new(%$ua_params);

  if ($self->granularity !~ /^d/i) {
    $start_date = $self->snap_start_date($start_date);
    $end_date   = $self->snap_end_date($end_date);
  }

  $self->start_date($start_date);
  $self->end_date($end_date);
  $self->symbols(@$symbols);

  # These are used for constructing method names for target types.
  $self->{target_order} = [qw(quote split dividend)];
  grep($self->{targets}{$_} = "${_}s", @{$self->{target_order}});

  $self;
}

### User interface stubs

sub quotes    { shift->getter(target_mode => 'quote')->()    }
sub dividends { shift->getter(target_mode => 'dividend')->() }
sub splits    { shift->getter(target_mode => 'split')->()    }
sub intraday  { shift->getter(target_mode => 'intraday')->() }

*intraday_quotes = *intraday;

sub target_worthy {
  my $self = shift;
  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $parse_mode  = $parms{parse_mode}  || $self->parse_mode;
  # forcing url_maker into a boolean role here, using a dummy symbol
  my $capable = $self->url_maker(
    %parms,
    target_mode => $target_mode,
    parse_mode  => $parse_mode,
    symbol      => 'waggledance',
  );
  my $worthy = $capable && UNIVERSAL::isa($capable, 'CODE');
  if ($self->{verbose}) {
    print STDERR "Seeing if ", ref $self,
                 " can get ($target_mode, $parse_mode) : ",
                 $worthy ? "yes\n" : "no\n";
  }
  $worthy;
}

sub granularities { qw( daily ) }

### Data retrieval

sub ua {
  my $self = shift;
  @_ ? $self->{ua} = shift : $self->{ua};
}

sub fetch {
  # HTTP::Request and LWP::UserAgent Wrangler
  my($self, $request) = splice(@_, 0, 2);
  $request or croak "Request or URL required\n";

  if (! ref $request || ! $request->isa('HTTP::Request')) {
    $request = HTTP::Request->new(GET => $request);
  }

  my $trys = $self->{attempts};
  my $response = $self->ua->request($request, @_);
  $self->{_lwp_success} = 0;
  while (! $response->is_success) {
    last unless $trys;
    $self->{_lwp_status}  = $response->status_line;
    print STDERR "Bad fetch",
       $response->is_error ? ' (' . $response->status_line . '), ' : ', ',
       "trying again...\n" if $self->{debug};
    $response = $self->ua->request($request, @_);
    --$trys;
  }
  $self->{_lwp_success} = $response->is_success;
  return undef unless $response->is_success;
  print STDERR 'Fetch complete. (' . length($response->content) . " chars)\n"
    if $self->{verbose};
  $response->content;
}

sub getter {
  # closure factory to get results for a particular target_mode and
  # parse_mode
  my $self = shift;

  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $parse_mode  = $parms{parse_mode}  || $self->parse_mode;
  my @column_labels = $self->labels(
    %parms, target_mode => $target_mode, parse_mode  => $parse_mode
  );
  my %extractors = $self->extractors(
    %parms, target_mode => $target_mode, parse_mode  => $parse_mode
  );

  # return our closure
  sub {
    my @symbols = @_ ? @_ : $self->symbols;

    my @rows;

    # cache check
    my @not_seen;
    foreach my $symbol (@symbols) {
      my @r = $self->result_rows($target_mode, $symbol);
      if (@r) {
        push(@rows, @r);
      }
      else {
        push(@not_seen, $symbol);
      }
    }
    return @rows unless @not_seen;

    my $original_target_mode = $self->target_mode;
    my $original_parse_mode  = $self->parse_mode;

    $self->target_mode($target_mode);
    $self->parse_mode($parse_mode);

    my $dcol = $self->label_column('date');
    my(%empty_fetch, %saw_good_rows);
    my $last_data = '';

    my $target_worthy = $self->target_worthy(
      %parms,
      target_mode => $target_mode,
      parse_mode  => $parse_mode
    );
    if (!$target_worthy) {
      # make sure and empty @symbols
      ++$empty_fetch{$_} while $_ = pop @symbols;
    }

    SYMBOL: foreach my $s (@symbols) {
      my $urlmaker = $self->url_maker(
        target_mode => $target_mode,
        parse_mode  => $parse_mode,
        symbol      => $s,
      );
      UNIVERSAL::isa($urlmaker, 'CODE') or croak "urlmaker not a code ref.\n";
      my $so_far_so_good = 0;
      URL: while (my $url = $urlmaker->()) {
        if ($empty_fetch{$s}) {
          print STDERR ref $self,
             " passing on $s ($target_mode) for now, empty fetch\n"
             if $self->{verbose};
          last URL;
        }

        if ($self->{verbose}) {
          my $uri = $url;
          $uri = $url->uri if UNIVERSAL::isa($url, 'HTTP::Request');
          print STDERR "Processing ($s:$target_mode) $uri\n";
        }

        # We're a bit more persistent with quotes. It is more suspicious
        # if we get no quote rows, but it is nevertheless possible.
        my $trys = $target_mode eq 'quote' ? $self->{attempts} : 1;
        my $initial_trys = $trys;
        my($data, $rows) = ('', []);
        do {
          print STDERR "$s Trying ($target_mode) again due to no rows...\n"
            if $self->{verbose} && $trys != $initial_trys;
          if (!($data = $self->{url_cache}{$url})) {
            $data = $self->fetch($url);
            if (my $pre_parser = $self->pre_parser) {
              $data = $pre_parser->(
                $data,
                target_mode => $target_mode,
                parse_mode  => $parse_mode,
              );
            }
          }
          # make sure our url_maker hasn't sent us into a twister
          if ($data && $data eq $last_data) {
            print STDERR "Redundant data fetch, assuming end of URLs.\n"
              if $self->{verbose};
            last URL;
          }
          else {
            $last_data = defined $data ? $data : '';
          }
          $rows = $self->rows($self->parser->($data));
          last URL if $so_far_so_good && !@$rows;
          --$trys;
        } while !@$rows && $trys && $self->{_lwp_success};
        $so_far_so_good = 1;

        if ($target_mode ne 'quote' || $target_mode ne 'intraday') {
          # We are not very stubborn about dividends and splits right
          # now. This is because we cannot prove a successful negative
          # (i.e., say there were no dividends or splits over the time
          # period...or perhaps there were, but it is a defunct
          # symbol...whatever...quotes should always be present unless
          # they are defunct, which is dealt with later.
          if (!$self->{_lwp_success} || !$data) {
            ++$empty_fetch{$s};
            @$rows = ();
          }
          elsif ($self->{_lwp_success} && !@$rows) {
            ++$empty_fetch{$s};
          }
        }

        # Raw cache
        $self->{url_cache}{$url} = $data;
  
        # Extraction filters. This is an opportunity to extract rows
        # that are not what we are looking for, but contain valuable
        # information nevertheless. An example of this would be the
        # split and dividend rows you see in Yahoo HTML quote output. An
        # extraction filter method should expect an array ref as an
        # argument, representing a single row, and should return another
        # array ref with extracted output. If there is a return value,
        # then this row will be filtered from the primary output.
        my(%extractions, $ecount, $rc);
        $rc = @$rows;
        if (%extractors) {
          my(@filtered, $row);
          while ($row = pop(@$rows)) {
            my $erow;
            foreach my $mode (sort keys %extractors) {
              $extractions{$mode} ||= [];
              my $em = $extractors{$mode};
              if ($erow = $em->($row)) {
                print STDERR "$s extract ($mode) got $s, ",
                   join(', ', @$erow), "\n" if $self->{verbose};
                push(@{$extractions{$mode}}, [@$erow]);
                ++$ecount;
                last;
              }
            }
            push(@filtered, $row) unless $erow;
          }
          if ($self->{verbose} && $ecount) {
            print STDERR "$s Trimmed to ",$rc - $ecount,
               " rows after $ecount extractions.\n";
          }
          $rows = \@filtered;
        }

        if ($extractions{$target_mode}) {
          $rows = [@{$extractions{$target_mode}}];
          print STDERR "Coopted to ", scalar @$rows,
            " rows after $target_mode extraction redundancy.\n"
            if $self->{verbose};
        }

        if (@$rows) {
          # Normalization steps

          if ($target_mode eq 'split') {
            if (@{$rows->[0]} == 2) {
              foreach (@$rows) {
                if ($_->[-1] =~ /(split\s+)?(\d+)\D+(\d+)/is) {
                  splice(@$_, -1, 1, $2, $3);
                }
              }
            }
          }

          # Saving the rounding operations until after the adjust
          # routine is deliberate since we don't want to be auto-
          # adjusting pre-rounded numbers.
          $self->number_normalize_rows($rows);
  
          # Do the same for the extraction rows, plus store the
          # extracted rows
          foreach my $mode (keys %extractions) {
            # _store_results splices each row...don't do it twice
            next if $mode eq $target_mode;
            $self->target_mode($mode);
            $self->number_normalize_rows($extractions{$mode});
            $self->_target_source($mode, $s, ref $self);
            $self->_store_results($mode, $s, $dcol, $extractions{$mode});
          }
          # restore original target mode
          $self->target_mode($target_mode);
  
          if ($target_mode eq 'quote' || $target_mode eq 'intraday') {
            my $count = @$rows;
            @$rows = grep($self->is_quote_row($_) &&
                          $self->row_not_seen($s, $_), @$rows);
            if ($self->{verbose}) {
              if ($count == @$rows) {
                print STDERR "$s Retained $count rows\n";
              }
              else {
                print STDERR "$s Retained $count raw rows, trimmed to ",
                  scalar @$rows, " rows due to noise\n";
              }
            }
  
          }
          if ($target_mode eq 'quote') {
            # zcount is an attempt to capture null values; if there are
            # too many we assume there is something wrong with the
            # remote data
            my $close_col = $self->label_column('close');
            my($zcount, $hcount) = (0,0);
            foreach (@$rows) {
              foreach (@$_) {
                # Sometimes N/A appears
                s%^\s*N/A\s*$%%;
              }
              my $q = $_->[$close_col];
              if (defined $q && $q =~ /\d+/) { ++$hcount }
              else                            { ++$zcount }
            }
            my $pct = $hcount ? 100 * $zcount / ($zcount + $hcount) : 100;
            if (!$trys || $pct >= $self->{zthresh}) {
              ++$empty_fetch{$s} unless $saw_good_rows{$s};
            }
            else {
              # For defunct symbols, we could conceivably get quotes
              # over a date range that contains blocks of time where the
              # ticker was actively traded, as well as blocks of time
              # where the ticker doesn't exist. If we got good data over
              # some of the blocks, then we take note of it so we don't
              # toss the whole set of queries for this symbol.
              ++$saw_good_rows{$s};
            }
            $self->precision_normalize_rows($rows)
              if @$rows && $self->{quote_precision};
          }

          last URL if !$ecount && !@$rows;
          $self->_store_results($target_mode, $s, $dcol, $rows) if @$rows;
          $self->_target_source($target_mode, $s, ref $self);
        }
      }
    }

    $self->_store_empty_fetches([keys %empty_fetch]);
  
    # Check for bad fetches. If we failed on some symbols, punt them to
    # our champion class.
    if (%empty_fetch) {
      my @bad_symbols = $self->empty_fetches;
      my @champion_classes = $self->lineup;
      while (@champion_classes && @bad_symbols) {
        print STDERR "Bad fetch for ", join(',', @bad_symbols), "\n"
          if $self->{verbose} && $target_worthy;
        my $champion =
          $self->_summon_champion(shift @champion_classes, @bad_symbols);
        next unless $champion &&
                    $champion->target_worthy(target_mode => $target_mode);
        print STDERR ref $champion, ", my hero!\n" if $self->{verbose};
        # Hail Mary
        $champion->getter(target_mode => $target_mode)->();
        # Our champion was the source for these symbols (including
        # extracted info).
        foreach my $mode ($champion->result_modes) {
          foreach my $symbol ($champion->result_symbols($mode)) {
            $self->_target_source($mode, $symbol, ref $champion);
            $self->_copy_results($mode, $symbol,
                                 $champion->results($mode, $symbol));
          }
        }
        @bad_symbols = $champion->empty_fetches;
      }
      if (@bad_symbols && !$self->{quiet}) {
        print STDERR "WARNING: Could not fetch $target_mode for some symbols (",join(', ', @bad_symbols), "). Abandoning request for these symbols.";
        if ($target_mode ne 'quote') {
          print STDERR " Don't worry, though, we were looking for ${target_mode}s. These are less likely to exist compared to quotes.";
        }
        if ($self->{_lwp_status}) {
          print STDERR "\n\nLast status: $self->{_lwp_status}\n";
        }
        print STDERR "\n";
      }
    }
  
    $self->target_mode($original_target_mode);
    $self->parse_mode($original_parse_mode);

    @rows = $self->result_rows($target_mode);
    if ($self->{verbose}) {
      print STDERR "Class ", ref $self, " returning ", scalar @rows,
         " composite rows.\n";
    }

    # Return the loot.
    wantarray ? @rows : \@rows;
  };
}

sub _store_results {
  my($self, $mode, $symbol, $dcol, $rows) = @_;
  foreach my $row (@$rows) {
    my $date = splice(@$row, $dcol, 1);
    $self->{results}{$mode}{$symbol}{$date} = $row;
  }
}

sub _copy_results {
  my($self, $mode, $symbol, $results) = @_;
  foreach my $date (sort keys %$results) {
    $self->{results}{$mode}{$symbol}{$date} = [@{$results->{$date}}];
  }
}

sub result_rows {
  my($self, $target_mode, @symbols) = @_;
  $target_mode ||= $self->target_mode;
  @symbols = $self->result_symbols($target_mode) unless @symbols;
  my @rows;
  foreach my $symbol (@symbols) {
    my $results = $self->results($target_mode, $symbol);
    foreach my $date (sort keys %$results) {
      push(@rows, [$symbol, $date, @{$results->{$date}}]);
    }
  }
  sort { $a->[1] cmp $b->[1] } @rows;
}

sub _store_empty_fetches {
  my $self = shift;
  my $ref = shift || [];
  @$ref = sort @$ref;
  $self->{empty_fetches} = $ref;
}

sub empty_fetches {
  my $self = shift;
  return () unless $self->{empty_fetches};
  @{$self->{empty_fetches}} 
}

sub extractors { () }

sub rows {
  my($self, $rows) = @_;
  return wantarray ? () : [] unless $rows;
  my $rc = @$rows;
  print STDERR "Got $rc raw rows\n" if $self->{verbose};

  # Load user filter if present
  my $row_filter = $self->row_filter;

  # Prep the rows
  foreach my $row (@$rows) {
    $row_filter->($row) if $row_filter;
    foreach (@$row) {
      # Zap leading and trailing white space
      next unless defined;
      s/^\s+//; s/\s+$//;
    }
  }
  # Pass only rows with a valid date that is in range (and store the
  # processed value while we are at it)
  my $target_mode = $self->target_mode;
  my @date_rows;
  my $dcol = $self->label_column('date');
  my $tcol = $self->label_column('time') if $target_mode eq 'intraday';
  my $r;
  while($r = pop @$rows) {
    my $date = $r->[$dcol];
    if ($target_mode eq 'intraday') {
      my $time = splice(@$r, $tcol, 1);
      $date = join('', $date, $time);
    }
    $date = $self->date_normalize($date);
    unless ($date) {
      print STDERR "Reject row (no date): '$r->[$dcol]'\n" if $self->{verbose};
      next;
    }
    next unless $self->date_in_range($date);
    $r->[$dcol] = $date;
    push(@date_rows, $r);
  }

  print STDERR "Trimmed to ", scalar @date_rows, " applicable date rows\n"
    if $self->{verbose} && @date_rows != $rc;

  return wantarray ? @date_rows : \@date_rows;
}

### Adjustment triggers and manipulation

sub adjuster {
  # In order to be an adjuster, it must first be enabled. In addition,
  # there has to be a column specified as the adjusted value. This is
  # not as generic as I would like it, but so far it's just for
  # Yahoo...it should work for any site with "adj" in the column
  # label...this column should be the adjusted closing value.
  my $self = shift;
  return 0 if !$self->{adjusted};
  foreach ($self->labels) {
    return 1 if /adj/i;
  }
  0;
}

sub adjusted { shift->{adjusted} ? 1 : 0 }

### Bulk manipulation filters

sub date_normalize_rows {
  # Place dates into a consistent format, courtesy of Date::Manip
  my($self, $rows, $dcol) = @_;
  $dcol = $self->label_column('date') unless defined $dcol;
  foreach my $row (@$rows) {
    $row->[$dcol] = $self->date_normalize($row->[$dcol]);
  }
  $rows;
}

sub date_normalize {
  my($self, $date) = @_;
  return unless $date;
  my $normal_date;
  if ($self->granularity =~ /^m/ && $date =~ m{^\s*(\D+)[-/]+(\d{2,})\s*$}) {
    my($m, $y) = ($1, $2);
    $y += 1900 if length $y == 2;
    $normal_date = ParseDate($m =~ /^\d+$/ ? "$y/$m/01" : "$m 01 $y");
  }
  else {
    # allow for negative epochs
    if ($date =~ /^-?\d+$/) {
      $normal_date = ParseDateString("epoch $date");
    }
    else {
      $normal_date = ParseDate($date);
    }
  }
  $normal_date or return undef;
  return $normal_date if $self->target_mode eq 'intraday';
  join('/', $self->ymd($normal_date));
}

sub snap_start_date {
  my($self, $date) = @_;
  my $g = $self->granularity;
  if ($g =~ /^(m|w)/i) {
    if ($1 eq 'm') {
      my($dom) = UnixDate($date, '%d') - 1;
      $date = DateCalc($date, "- $dom days") if $dom;
    }
    else {
      my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
      $date = DateCalc($date, "- $dow days") if $dow;
    }
  }
  $date;
}

sub snap_end_date {
  my($self, $date) = @_;
  my $g = $self->granularity;
  if ($g =~ /^(m|w)/i) {
    if ($1 eq 'm') {
      my($m, $y) = UnixDate($date, '%m', '%Y');
      my $last = Date_DaysInMonth($m, $y);
      $date = ParseDateString("$y$m$last");
    }
    else {
      my $dow = Date_DayOfWeek(UnixDate($date, '%m', '%d', '%Y')) - 1;
      $date = DateCalc($date, "+ " . (6 - $dow) . ' days')
        unless $dow == 6;
    }
  }
  $date;
}

sub number_normalize_rows {
  # Strip non-numeric noise from numeric fields
  my($self, $rows, $dcol) = @_;
  $dcol = $self->label_column('date') unless defined $dcol;
  # filtered rows might not have same columns
  my @cols = grep($_ != $dcol, 0 .. $#{$rows->[0]});
  foreach my $row (@$rows) {
    s/[^\d\.]//go foreach @{$row}[@cols];
  }
  $rows;
}

sub precision_normalize_rows {
  # Round off numeric fields, if requested (%.4f by default). Volume
  # is the exception -- we just round that into an integer. This
  # should probably only be called for 'quote' targets because it
  # knows details about where the numbers of interest reside.
  my($self, $rows) = @_;
  my $target_mode = $self->target_mode;
  croak "precision_normalize invoked in '$target_mode' mode rather than 'quote'  or 'intraday' mode.\n"
    unless $target_mode eq 'quote' || $target_mode eq 'intraday';
  my @columns;
  if ($target_mode ne 'intraday') {
    @columns = $self->label_column(qw(open high low close));
    push(@columns, $self->label_column('adj')) if $self->adjuster;
  }
  else {
    @columns = $self->label_column(qw(high low close));
  }
  my $vol_col = $self->label_column($Default_Vol_Pat);
  foreach my $row (@$rows) {
    $row->[$_] = sprintf("%.$self->{quote_precision}f", $row->[$_])
      foreach @columns;
    $row->[$vol_col] = int $row->[$vol_col];
  }
  $rows;
}

### Single row filters

sub is_quote_row {
  my($self, $row, $dcol) = @_;
  ref $row or croak "Row ref required\n";
  # Skip date in first field
  $dcol = $self->label_column('date') unless defined $dcol;
  foreach (0 .. $#$row) {
    next if $_ == $dcol;
    next if $row->[$_] =~ /^\s*$/;
    if ($row->[$_] !~ /^\s*\$*[\d\.,]+\s*$/) {
      return 0;
    }
  }
  1;
}

sub row_not_seen {
  my($self, $symbol, $row, $dcol) = @_;
  ref $row or croak "Row ref required\n";
  $symbol or croak "ticker symbol required\n";
  my $mode = $self->target_mode;
  my $res = $self->{results}{$mode} or return 1;
  my $mres = $res->{$symbol} or return 1;
  $dcol = $self->label_column('date') unless defined $dcol;
  $mres->{$row->[$dcol]} or return 1;
  return 0;
}

sub date_in_range {
  my $self = shift;
  my $date = shift;
  $date = $self->date_standardize($date) or return undef;
  return 0 if $self->{start_date} && $date lt $self->{start_date};
  return 0 if $self->{end_date}   && $date gt $self->{end_date};
  1;
}

### Label and label mapping/extraction management

sub default_target_mode { $Default_Target_Mode }
sub default_parse_mode  { $Default_Parse_Mode  }
sub default_granularity { $Default_Granularity }

sub set_label_pattern {
  my $self = shift;
  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $parse_mode  = $parms{parse_mode}  || $self->parse_mode;
  my $label = $parms{label};
  croak "Column label required\n" unless $label;
  my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
  my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
  my $pattern = $parms{pattern};
  if ($pattern) {
    $l2p->{$label} = $pattern;
    delete $self->{label_map};
    delete $self->{pattern_map};
  }
  my $pat = $l2p->{$label} ||= ($label =~ $Default_Vol_Pat ?
                                  qr/\s*$label/i : qr/^\s*$label/i);
  $p2l->{$pat} ||= $label;
  $pat;
}

sub label_pattern {
  my $self = shift;
  my $target_mode = $self->target_mode;
  my $parse_mode  = $self->parse_mode;
  my $label = shift;
  croak "column label required\n" unless $label;
  my $l2p = $self->{_label_pat}{$target_mode}{$parse_mode} ||= {};
  my $pat = $l2p->{$label} || $self->set_label_pattern(label => $label);
  $pat;
}

sub label_column {
  my $self = shift;
  my @cols;
  if (!$self->{label_map}) {
    delete $self->{pattern_map};
    my @labels = $self->labels;
    foreach my $i (0 .. $#labels) {
      $self->{label_map}{$labels[$i]} = $i;
    }
  }
  foreach (@_) {
    croak "Unknown label '$_'\n" unless exists $self->{label_map}{$_};
    push(@cols, $self->{label_map}{$_});
  }
  unless (wantarray) {
    croak "multiple columns in scalar context\n" if @cols > 1;
    return $cols[0];
  }
  @cols;
}

sub pattern_column {
  my $self = shift;
  if (!$self->{pattern_map}) {
    my @patterns = $self->patterns;
    foreach my $i (0 .. $#patterns) {
      $self->{pattern_map}{$patterns[$i]} = $i;
    }
  }
  return unless @_;
  my $pattern = shift;
  croak "Unknown pattern '$pattern'\n" unless $self->{_pat_map}{$pattern};
  $self->{pattern_map{$pattern}};
}

sub pattern_map {
  my $self = shift;
  $self->pattern_column unless $self->{pattern_map};
  $self->{pattern_map};
}

sub label_map {
  my $self = shift;
  $self->label_column unless $self->{label_map};
  $self->{label_map};
}

sub pattern_label {
  my $self = shift;
  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $parse_mode  = $parms{parse_mode}  || $self->parse_mode;
  my $pat = $parms{pattern} or croak "pattern required for label lookup\n";
  my $p2l = $self->{_pat_label}{$target_mode}{$parse_mode} ||= {};
  my $label = $p2l->{$pat};
  unless (defined $label) {
    delete $parms{pattern};
    $self->set_label_pattern(%parms, label => $_) foreach $self->labels;
  }
  $label;
}

sub patterns {
  my $self = shift;
  my %parms = @_;
  $parms{target_mode} ||= $self->target_mode;
  $parms{parse_mode}  ||= $self->parse_mode;
  map($self->label_pattern($_), $self->labels(%parms));
}

sub columns {
  my $self = shift;
  my %parms = @_;
  $parms{target_mode} ||= $self->target_mode;
  $parms{parse_mode}  ||= $self->parse_mode;
  $self->label_column($self->labels(%parms));
}

sub default_labels {
  my $self = shift;
  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $tm = $Default_Labels{$target_mode};
  unless ($tm) {
    $tm = $Default_Labels{$self->default_target_mode};
  }
  my $parse_mode = $parms{parse_mode} || $self->parse_mode;
  my $labels = $tm->{$parse_mode};
  unless ($labels) {
    $labels = $tm->{$self->default_parse_mode};
  }
  @$labels;
}

sub labels {
  my $self  = shift;
  my %parms = @_;
  my $target_mode = $parms{target_mode} || $self->target_mode;
  my $parse_mode  = $parms{parse_mode}  || $self->parse_mode;
  my $tm = $self->{_labels}{$target_mode};
  if ($parms{labels} || ! $tm->{$parse_mode}) {
    delete $self->{label_map};
    delete $self->{pattern_map};
  }
  $tm->{$parse_mode} = $parms{labels} if $parms{labels};
  my $labels = $tm->{$parse_mode} ||= [$self->default_labels(
                                         target_mode => $target_mode,
                                         parse_mode  => $parse_mode)];
  @$labels;
}

sub parse_mode {
  my $self = shift;
  if (@_) {
    $self->{parse_mode} = shift;
  }
  $self->{parse_mode} || $self->default_parse_mode;
}

sub target_mode {
  my $self = shift;
  if (@_) {
    $self->{target_mode} = shift;
  }
  $self->{target_mode} || $self->default_target_mode;
}

sub granularity {
  my $self = shift;
  if (@_) {
    $self->{granularity} = shift;
  }
  $self->{granularity} || $self->default_granularity;
}

sub lineup {
  my $self = shift;
  $self->{lineup} = \@_ if @_;
  return unless $self->{lineup};
  @{$self->{lineup}};
}

### Parser methods

sub pre_parser {
  my($self, %parms) = @_;
  my $parse_mode = $parms{parse_mode} || $self->parse_mode;
  my $method = "${parse_mode}_pre_parser";
  return unless $self->can($method);
  $self->$method(%parms, parse_mode => $parse_mode);
}

sub parser {
  my($self, %parms) = @_;
  my $parse_mode = $parms{parse_mode} || $self->parse_mode;
  my $make_parser = "${parse_mode}_parser";
  $self->$make_parser(%parms, parse_mode => $parse_mode);
}

sub html_parser {
  # HTML::TableExtract supports automatic column reordering.
  my $self = shift;
  my $class = HTML_CLASS;
  my @labels = $self->labels(@_);
  my @patterns = $self->patterns(@_);
  my(%pat_map, %label_map);
  $pat_map{$patterns[$_]} = $_ foreach 0 .. $#patterns;
  $label_map{$labels[$_]} = $_ foreach 0 .. $#labels;
  $self->pattern_map(\%pat_map);
  $self->label_map(\%label_map);
  sub {
    my $data = shift;
    my $html_string;
    if (ref $data) {
      local($/);
      $html_string = <$data>;
    }
    else {
      $html_string = $data;
    }
    my %te_parms = (
      headers => \@patterns,
      automap => 1,
    );
    $te_parms{debug} = $self->{debug} if $self->{debug} > 2;
    my $te = $class->new(%te_parms) or croak "Problem creating $class\n";
    $te->parse($html_string);
    $te->eof;
    my $ts = $te->first_table_found;
    [ $ts ? $ts->rows() : ()];
  }
}

sub csv_parser {
  # Text::CSV_XS doesn't column slice or re-order, so we do.
  my $self = shift;
  my @patterns = $self->patterns(@_);
  sub {
    my $data = shift;
    return [] unless defined $data;
    my @csv_lines = ref $data ? <$data> : split("\n", $data);
    # BOM squad (byte order mark, as csv from google tends to be)
    if ($csv_lines[0] =~ s/^\xEF\xBB\xBF//) {
      for my $i (0 .. $#csv_lines) {
        utf8::decode($csv_lines[$i]);
      }
    }
    # might be unix, windows, or mac style newlines
    s/\s+$// foreach @csv_lines;
    return [] if !@csv_lines || $csv_lines[0] =~ /(no data)|error/i;
    # attempt to get rid of comments at front of csv data
    while (@csv_lines) {
      last if $csv_lines[0] =~ /date/i || $csv_lines[0] =~ /\d+$/;
      print STDERR "CSV reject line: $csv_lines[0]\n" if $self->{verbose};
      shift @csv_lines;
    }
    my $first_line = $csv_lines[0];
    my $sep_char = $first_line =~ /date\s*(\S)/i ? $1 : ',';
    my $cp = $CSV_Class->new({sep_char => $sep_char, binary => 1})
      or croak "Problem creating $CSV_Class\n";
    my @pat_slice;
    if ($first_line =~ /date/i) {
      # derive column detection and ordering
      $cp->parse($first_line) or croak ("Problem parsing (" .
        $cp->error_input . ") : " . $cp->error_diag . "\n");
      my @headers = $cp->fields;
      my @pats = @patterns;
      my @labels = map($self->pattern_label(pattern => $_), @patterns);
      my(%pat_map, %label_map);
      HEADER: for my $i (0 .. $#headers) {
        last unless @pats;
        my $header = $headers[$i];
        for my $pi (0 .. $#pats) {
          my $pat = $pats[$pi];
          if ($header =~ /$pat/) {
            my $label = $labels[$pi];
            splice(@pats,   $pi, 1);
            splice(@labels, $pi, 1);
            $pat_map{$pat} = $i;
            $label_map{$label} = $i;
            next HEADER;
          }
        }
      }
      shift @csv_lines;
      @pat_slice = map($pat_map{$_}, @patterns);
    }
    else {
      # no header row, trust natural order and presence
      @pat_slice = 0 .. $#patterns;
    }
    my @rows;
    foreach my $line (@csv_lines) {
      $cp->parse($line) or next;
      my @fields = $cp->fields;
      push(@rows, [@fields[@pat_slice]]);
    }
    \@rows;
  };
}

### Accessors, generators

sub start_date {
  my $self = shift;
  if (@_) {
    my $start_date = shift;
    my $clear = @_ ? shift : 1;
    $self->clear_cache if $clear;
    $self->{start_date} = defined $start_date ?
      $self->date_standardize($start_date) : undef;
  }
  $self->{start_date};
}

sub end_date {
  my $self = shift;
  if (@_) {
    my $end_date = shift;
    my $clear = @_ ? shift : 1;
    $self->clear_cache if $clear;
    $self->{end_date} = defined $end_date ?
      $self->date_standardize($end_date) : undef;
  }
  $self->{end_date};
}

sub date_standardize {
  my($self, @dates) = @_;
  return unless @dates;
  foreach (@dates) {
    $_ = ParseDate($_) or Carp::confess "Could not parse date '$_'\n";
    s/\d\d:.*//;
  }
  @dates > 1 ? @dates : ($dates[0]);
}

sub mydates {
  my $self = shift;
  $self->dates($self->{start_date}, $self->{end_date});
}

sub dates {
  my($self, $sdate, $edate) = @_;
  $sdate && $edate or croak "Start date and end date strings required\n";
  my($sd, $ed) = sort($self->date_standardize($sdate, $edate));
  my @dates;
  push(@dates, $sd) if Date_IsWorkDay($sd);
  my $cd = $self->date_standardize(Date_NextWorkDay($sd, 1));
  while ($cd <= $ed) {
    push(@dates, $cd);
    $cd = $self->date_standardize(Date_NextWorkDay($cd));
  }
  @dates;
}

sub symbols {
  my($self, @symbols) = @_;
  if (@symbols) {
    my %seen;
    grep(++$seen{$_}, grep(uc $_, @symbols));
    $self->{symbols} = [sort keys %seen];
    $self->clear_cache;
  }
  @{$self->{symbols}};
}

sub successors {
  my $self = shift;
  @{$self->{successors}};
}

sub clear_cache {
  my $self = shift;
  delete $self->{url_cache};
  delete $self->{results};
  1;
}

sub result_modes {
  my $self = shift;
  return () unless $self->{results};
  sort keys %{$self->{results}};
}

sub result_symbols {
  my($self, $target_mode) = @_;
  $target_mode ||= $self->target_mode;
  return () unless $self->{sources}{$target_mode};
  sort keys %{$self->{results}{$target_mode}};
}

sub results {
  my($self, $target_mode, $symbol) = @_;
  $self->{results}{$target_mode}{$symbol};
}

sub quote_source    { shift->source(shift, 'quote')    }
sub dividend_source { shift->source(shift, 'dividend') }
sub split_source    { shift->source(shift, 'split')    }
sub intraday_source { shift->source(shift, 'intraday') }

sub row_filter { shift->{row_filter} }

sub source {
  my($self, $symbol, $target_mode) = @_;
  croak "Ticker symbol required\n" unless $symbol;
  $target_mode ||= $self->target_mode;
  $self->{sources}{$target_mode}{$symbol} || '';
}

sub _target_source {
  my($self, $target_mode, $symbol, $source) = @_;
  croak "Target mode required\n"   unless $target_mode;
  croak "Ticker symbol required\n" unless $symbol;
  $symbol = uc $symbol;
  if ($source) {
    $self->{sources}{$target_mode}{$symbol} = $source;
  }
  $self->{sources}{$target_mode}{$symbol};
}

###

sub _summon_champion {
  # Instantiate the next class in line if this class failed in
  # fetching any quotes. Make sure and pass along the remaining
  # champions to the new champion.
  my($self, $champion_class, @bad_symbols) = @_;
  return undef unless ref $self->{lineup} && @{$self->{lineup}};
  print STDERR "Loading $champion_class\n" if $self->{verbose};
  eval "require $champion_class;";
  die $@ if $@;
  my $champion = $champion_class->new
    (
     symbols    => [@bad_symbols],
     start_date => $self->{start_date},
     end_date   => $self->{end_date},
     adjusted   => $self->{adjusted},
     verbose    => $self->{verbose},
     lineup     => [],
    );
  $champion;
}

### Toolbox

sub save_query    { shift->_save_restore_query(1) }
sub restore_query { shift->_save_restore_query(0) }
sub _save_restore_query {
  my($self, $save) = @_;
  $save = 1 unless defined $save;
  foreach (qw(parse_mode target_mode start_date end_date granularity quiet)) {
    my $qstr = "_query_$_";
    if ($save) {
      $self->{$qstr} = $self->{$_};
    }
    else {
      $self->{$_} = $self->{$qstr} if exists $self->{$qstr};
    }
  }
  $self;
}

sub ymd {
  my $self = shift;
  my @res = $_[0] =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
  shift =~ /^\s*(\d{4})(\d{2})(\d{2})/o;
}

sub date_iterator {
  my $self = shift;
  my %parms = @_;
  my $start_date = $parms{start_date};
  my $end_date   = $parms{end_date} || 'today';
  my $increment  = $parms{increment};
  my $units      = $parms{units} || 'days';
  $increment && $increment > 0 or croak "Increment > 0 required\n";
  $start_date = ParseDate($start_date) if $start_date;
  $end_date   = ParseDate($end_date)   if $end_date;
  if ($start_date && $start_date gt $end_date) {
    ($start_date, $end_date) = ($end_date, $start_date);
  }
  my($low_date, $high_date);
  $high_date = $end_date;
  sub {
    return () unless $end_date;
    $low_date = DateCalc($high_date, "- $increment $units");
    if ($start_date && $low_date lt $start_date) {
      $low_date = $start_date;
      undef $start_date;
      undef $end_date;
      return () if $low_date eq $high_date;
    }
    my @date_pair = ($low_date, $high_date);
    $high_date = $low_date;
    @date_pair;
  }
}

1;

__END__

=head1 NAME

Finance::QuoteHist::Generic - Base class for retrieving historical stock quotes.

=head1 SYNOPSIS

  package Finance::QuoteHist::MyFavoriteSite;
  use strict;
  use vars qw(@ISA);
  use Finance::QuoteHist::Generic;
  @ISA = qw(Finance::QuoteHist::Generic);

  sub url_maker {
    # This method returns a code reference for a routine that, upon
    # repeated invocation, will provide however many URLs are necessary
    # to fully obtain the historical data for a given target mode and
    # parsing mode.
  }

=head1 DESCRIPTION

This is the base class for retrieving historical stock quotes. It is
built around LWP::UserAgent. Page results are currently parsed as either
CSV or HTML tables.

In order to actually retrieve historical stock quotes, this class should
be subclassed and tailored to a particular web site. In particular, the
C factory method should be overridden, which provides a
code reference to a routine that provides however many URLs are
necessary to retrieve the data over a list of symbols within the given
date range, for a particular target (quotes, dividends, splits).
Different sites have different formats and different limitations on how
many quotes are returned for each query. See Finance::QuoteHist::Yahoo
for an example of how to do this.

For more complicated sites, such as Yahoo, overriding additional methods
might be necessary for dealing with things such as splits and dividends.

=head1 METHODS

=over

=item new()

Returns a new Finance::QuoteHist::Generic object.  Valid attributes
are:

=over

=item start_date

=item end_date

Specify the date range from which you would like historical quotes.
These dates get parsed by the C method in Date::Manip, so
see L for more information on valid date strings. They
are quite flexible, and include such strings as '1 year ago'. Date
boundaries can also be dynamically set with methods of the same
name. The absence of a start date means go to the beginning of the
history. The absence of an end date means go up to the most recent
historical date. The absence of both means grab everything.

=item symbols

Indicates which ticker symbols to include in the search for historical
quotes. Passed either as a string (for single ticker) or an array ref
for multiple tickers.

=item granularity

Returns rows at 'daily', 'weekly', or 'monthly' levels of granularity.
Defaults to 'daily'.

=item attempts

Sets how persistently the module tries to retrieve the quotes. There are
two places this will manifest. First, if there are what appear to be
network errors, this many network connections are attempted for that
URL. Secondly, for quotes only, if pages were successfully retrieved,
but they contained no quotes, this number of attempts are made to
retrieve a document with data. Sometimes sites will report a temporary
internal error via HTML, and if it is truly transitory this will usually
get around it. The default is 3.

=item lineup

Passed as an array reference (or scalar for single class), this list
indicates which Finance::QuoteHist::Generic sub classes should be
invoked in the event this class fails in its attempt to retrieve
historical quotes. In the event of failure, the first class in this list
is invoked with the same parameters as the original class, and the
remaining classes are passed as the lineup to the new class. This sets
up a daisy chain of redundancy in the event a particular site is hosed.
See L to see an example of how this is done in a
top level invocation of these modules. This list is empty by default.

=item quote_precision

Sets the number of decimal places to which quote values are rounded.
This might be of particular significance if there is auto-adjustment
taking place (which is only under particular circumstances
currently...see L). Setting this to 0 will
disable the rounding behavior, returning the quote values as they
appear on the sites (assuming no auto-adjustment has taken place). The
default is 4.

=item row_filter

When provided a subroutine reference, the routine is invoked with an
array reference for each raw row retrieved from the quote source. This
allows user-defined filtering or formatting for the items of each
row. This routine is invoked before any built-in routines are called
on the row. The array must be modified directly rather than returned
as a value. Use sparingly since the built-in filtering and
normalizing routines do expect each row to more or less look like
historical stock data. Rearranging the order of the columns in each row
is contraindicated.

=item env_proxy

When set, instructs the underlying LWP::UserAgent to load proxy
configuration information from environment variables. See the C
method and L for more information.

=item auto_proxy

Same as env_proxy, but tests first to see if $ENV{http_proxy} is
present.

=item verbose

When set, many status messages are printed to STDERR indicating
progression through URLs and lineup invocations.

=item quiet

When set, certain failure messages are suppressed from appearing on
STDERR. These messages would normally appear regardless the setting of
the C flag.

=back

=back

The following methods are the primary user interface methods; methods
of interest to developers wishing to make their own site-specific
instance of this module will find information on overriding methods
further below.

=over

=item quotes()

Retrieves historical quotes for all provided symbols over the specified
date range. Depending on context, returns either a list of rows or an
array reference to the same list of rows.

=item dividends()

=item splits()

If available, retrieves dividend or split information for all provided
symbols over the specified date range. If there are no site-specific
subclassed modules in the B capable of getting dividends or
splits, the user will be notified on STDERR unless the B flag was
requested during object creation.

=item start_date(date_string)

=item end_date(date_string)

Set the date boundaries of all queries. The B is
interpreted by the Date::Manip module. The absence of a start date means
retrieve back to the beginning of that ticker's history. The absence of
an end date means retrieve up to the latest date in the history.

=item clear_cache()

When results are gathered for a particular date range, whether they be
via direct query or incidental extraction, they are cached. This cache
is cleared by invoking this method directly, by resetting the boundary
dates of the query, or by changing the C setting.

=item quote_source(ticker_symbol)

=item dividend_source(ticker_symbol)

=item split_source(ticker_symbol)

After query, these methods can be used to find out which particular
subclass in the B fulfilled the corresponding request for a
particular ticker symbol.

=back

The following methods are the primary methods of interest for developers
wishing to make a site-specific subclass. The url_maker() factory is
typically all that is necessary.

=over

=item url_maker()

Returns a subroutine reference that serves as an iterrator for producing
URLs based on target and parse mode. Repeated calls to this routine
produce subsequent URLs in the sequence.

=item extractors()

For a particular target mode and parse mode, returns a hash containing
code references to extraction routines for the remaining targets. For
example, for target 'quote' in parse mode 'html' there might be
extractor routines for both 'dividend' and 'split'.

=item ua()

Accessor method for the LWP::UserAgent object used to process
HTTP::Request for individual URLs. This can be handy for such things as
configuring proxy access for the underlying user agent. Example:

 # Manual configuration
 $qh1->ua->proxy(['http'], 'http://proxy.sn.no:8001/');

 # Load from environment variables
 $qh2->ua->env_proxy();

See L for more information on the capabilities
of that module.

=back

The following are potentially useful for calling within methods
overridden above:

=over

=item parse_mode($parse_mode)

Set the current parsing mode. Currently parsers are available for html
and csv.

=item target_mode($target_mode)

Return the current target mode.

=item dates($start_date, $end_date)

Returns a list of business days between and including the provided
boundary dates. If no arguments are provided, B and
B default to the currently specified date range.

=item labels(%parms)

Used to override the default labels for a given target mode and parse
mode. Takes the following named parameters:

=over

=item target_mode

Can currently be 'quote', 'dividend', or 'split'. Default is 'quote'.

=item parse mode

Can currently be 'csv' or 'html'. The default is typically 'csv' but
might vary depending on the quote source.

=item labels

The following are the default labels. Text entries convert to case-
insensitive regular expressions):

  target_mode
  -------------------------------------------------------
  quote    => ['date','open','high','low','close',qr(vol|shares)i]
  dividend => ['date','div']
  split    => ['date','post','pre']

=back

=back

=head1 DISCLAIMER

The data returned from these modules is in no way guaranteed, nor are
the developers responsible in any way for how this data (or lack
thereof) is used. The interface is based on URLs and page layouts that
might change at any time. Even though these modules are designed to be
adaptive under these circumstances, they will at some point probably be
unable to retrieve data unless fixed or provided with new parameters.
Furthermore, the data from these web sites is usually not even
guaranteed by the web sites themselves, and oftentimes is acquired
elsewhere. See the documentation for each site-specific module for more
information regarding the disclaimer for that site.

Above all, play nice.

=head1 AUTHOR

Matthew P. Sisk, EFE

=head1 COPYRIGHT

Copyright (c) 2000-2021 Matthew P. Sisk. All rights reserved. All wrongs
revenged. This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

Finance::QuoteHist(3), HTML::TableExtract(3), Date::Manip(3),
perlmodlib(1), perl(1).

=cut
Finance-QuoteHist-1.32/lib/Finance/QuoteHist.pm0000644000175000017500000001156314021707143020032 0ustar  sisksiskpackage Finance::QuoteHist;

# Simple aggregator for Finance::QuoteHist::Generic instances,
# the primary function of which is to specify the order in which
# to try the modules upon failure.

use strict;
use vars qw($VERSION $AUTOLOAD);
use Carp;

use Finance::QuoteHist::Generic;

$VERSION = '1.32';

my @DEFAULT_ENGINES = qw(
  Finance::QuoteHist::Yahoo
);

sub new {
  my $class = shift;
  my %parms = @_;
  if (!$parms{lineup}) {
    $parms{lineup} = [@DEFAULT_ENGINES];
  }
  elsif (! ref $parms{lineup}) {
    $parms{lineup} = [$parms{lineup}];
  }
  elsif (ref $parms{lineup} ne 'ARRAY') {
    croak "Lineup must be passed as an array ref or single-entry string\n";
  }

  # Instantiate the first, pass the rest as champions to the first
  my $first = shift @{$parms{lineup}};

  eval "require $first";
  croak $@ if $@;

  my $self = $first->new(%parms);

  $self;
}

sub default_lineup { @DEFAULT_ENGINES }

sub granularities { Finance::QuoteHist::Generic->granularities }

1;
__END__

=head1 NAME

Finance::QuoteHist - Perl module for fetching historical stock quotes.

=head1 SYNOPSIS

  use Finance::QuoteHist;
  $q = Finance::QuoteHist->new
     (
      symbols    => [qw(IBM UPS AMZN)],
      start_date => '01/01/2009', # or '1 year ago', see Date::Manip
      end_date   => 'today',
     );

  # Quotes
  foreach $row ($q->quotes()) {
    ($symbol, $date, $open, $high, $low, $close, $volume) = @$row;
    ...
  }

  # Splits
  foreach $row ($q->splits()) {
     ($symbol, $date, $post, $pre) = @$row;
  }

  # Dividends
  foreach $row ($q->dividends()) {
     ($symbol, $date, $dividend) = @$row;
  }

  # Culprit
  $fetch_class = $q->quote_source('IBM');

=head1 DESCRIPTION

Finance::QuoteHist is a top level interface for fetching historical
stock quotes from the web.

It is actually a front end to modules based on
Finance::QuoteHist::Generic, the main difference being that it has a
default I of web sites from which to attempt quote retrieval. If
the prospect of mixing data from multiple sites seems scary to you, then
use one of the site-specific modules directly.

Unless otherwise defined via the I attribute, this module will
select a I for you, the default being:

    Finance::QutoeHist::Yahoo
    Finance::QutoeHist::Google

Once instantiated, this module behaves identically to the first module
in the I, sharing all of that module's methods.

Most queries will likely be handled by the first module in the lineup.
If the site is down for some reason, or perhaps that site does not
provide quotes for defunct ticker symbols, then the other sites in the
lineup will be attempted.

See L for gory details on all of the
parameters and methods this module accepts. The basic interface is
noted below.

=head1 METHODS

The basic user interface consists of several methods, as seen in the
example above. Those methods are:

=over

=item quotes()

Returns a list of rows (or a reference to an array containing those
rows, if in scalar context). Each row contains the B, B,
B, B, B, B, and B for that date.
Optionally, if non-adjusted values were requested, their will be an
extra element at the end of the row for the B closing price.

=item dividends()

Returns a list of rows (or a reference to an array containing those
rows, if in scalar context). Each row contains the B and amount of
the B, in that order.

=item splits()

Returns a list of rows (or a reference to an array containing those
rows, if in scalar context). Each row contains the B, B
split shares, and B
 split shares, in that order.

=item source($ticker, $target)

Each of these methods displays which site-specific class actually
retrieved the information, if any, for a particular ticker symbol and
target such as 'quote' (default), 'dividend', or 'split'.

=back

=head1 DISCLAIMER

The data returned from these modules is in no way guaranteed, nor are
the developers responsible in any way for how this data (or lack
thereof) is used. The interface is based on URLs and page layouts that
might change at any time. Even though these modules are designed to be
adaptive under these circumstances, they will at some point probably be
unable to retrieve data unless fixed or provided with new parameters.
Furthermore, the data from these web sites is usually not even
guaranteed by the web sites themselves, and oftentimes is acquired
elsewhere. See the documentation for each site-specific module for more
information regarding the disclaimer for that site.

=head1 AUTHOR

Matthew P. Sisk, EFE

=head1 COPYRIGHT

Copyright (c) 2000-2021 Matthew P. Sisk. All rights reserved. All wrongs
revenged. This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

Finance::QuoteHist::Generic(3), Finance::QuoteHist::Yahoo(3), perl(1).

=cut
Finance-QuoteHist-1.32/Makefile.PL0000644000175000017500000000153714013775520015415 0ustar  sisksiskuse ExtUtils::MakeMaker;

my %prereq_pm = (
  'Date::Manip'        => 0,
  'LWP::UserAgent'     => 0,
  'HTTP::Request'      => 0,
  'HTML::TableExtract' => 2.07,
  'MIME::Base64'       => 0,
  'Text::CSV'          => 0,
  'JSON'               => 0,
);

eval "use Text::CSV_XS";
if ($@) {
  print STDERR <<__MSG;
Note: This is not required, but installing Text::CSV_XS on your system
      will speed up the parsing of quote data in CSV format. A C
      compiler is necessary, however. In the meantime we will use
      Text::CSV_PP.
__MSG
}
else {
  # why enforce the dependency if we already know it's present? Well, in
  # the future, we might want to enforce a version dependency here.
#  $prereq_pm{'Text::CSV_XS'} = 0;
}

WriteMakefile(
  NAME         => 'Finance-QuoteHist',
  VERSION_FROM => 'lib/Finance/QuoteHist.pm',
  PREREQ_PM    => \%prereq_pm,
);
Finance-QuoteHist-1.32/LICENSE0000644000175000017500000004343612453534725014462 0ustar  sisksiskThis is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

Terms of the Perl programming language system itself

a) the GNU General Public License as published by the Free
   Software Foundation; either version 1, or (at your option) any
   later version, or
b) the "Artistic License"

--- The GNU General Public License, Version 1, February 1989 ---

This is free software, licensed under:

  The GNU General Public License, Version 1, February 1989

                    GNU GENERAL PUBLIC LICENSE
                     Version 1, February 1989

 Copyright (C) 1989 Free Software Foundation, Inc.
                    51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The license agreements of most software companies try to keep users
at the mercy of those companies.  By contrast, our General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  The
General Public License applies to the Free Software Foundation's
software and to any other program whose authors commit to using it.
You can use it for your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Specifically, the General Public License is designed to make
sure that you have the freedom to give away or sell copies of free
software, that you receive source code or can get it if you want it,
that you can change the software or use pieces of it in new free
programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of a such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must tell them their rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License Agreement applies to any program or other work which
contains a notice placed by the copyright holder saying it may be
distributed under the terms of this General Public License.  The
"Program", below, refers to any such program or work, and a "work based
on the Program" means either the Program or any work containing the
Program or a portion of it, either verbatim or with modifications.  Each
licensee is addressed as "you".

  1. You may copy and distribute verbatim copies of the Program's source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and
disclaimer of warranty; keep intact all the notices that refer to this
General Public License and to the absence of any warranty; and give any
other recipients of the Program a copy of this General Public License
along with the Program.  You may charge a fee for the physical act of
transferring a copy.

  2. You may modify your copy or copies of the Program or any portion of
it, and copy and distribute such modifications under the terms of Paragraph
1 above, provided that you also do the following:

    a) cause the modified files to carry prominent notices stating that
    you changed the files and the date of any change; and

    b) cause the whole of any work that you distribute or publish, that
    in whole or in part contains the Program or any part thereof, either
    with or without modifications, to be licensed at no charge to all
    third parties under the terms of this General Public License (except
    that you may choose to grant warranty protection to some or all
    third parties, at your option).

    c) If the modified program normally reads commands interactively when
    run, you must cause it, when started running for such interactive use
    in the simplest and most usual way, to print or display an
    announcement including an appropriate copyright notice and a notice
    that there is no warranty (or else, saying that you provide a
    warranty) and that users may redistribute the program under these
    conditions, and telling the user how to view a copy of this General
    Public License.

    d) You may charge a fee for the physical act of transferring a
    copy, and you may at your option offer warranty protection in
    exchange for a fee.

Mere aggregation of another independent work with the Program (or its
derivative) on a volume of a storage or distribution medium does not bring
the other work under the scope of these terms.

  3. You may copy and distribute the Program (or a portion or derivative of
it, under Paragraph 2) in object code or executable form under the terms of
Paragraphs 1 and 2 above provided that you also do one of the following:

    a) accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of
    Paragraphs 1 and 2 above; or,

    b) accompany it with a written offer, valid for at least three
    years, to give any third party free (except for a nominal charge
    for the cost of distribution) a complete machine-readable copy of the
    corresponding source code, to be distributed under the terms of
    Paragraphs 1 and 2 above; or,

    c) accompany it with the information you received as to where the
    corresponding source code may be obtained.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form alone.)

Source code for a work means the preferred form of the work for making
modifications to it.  For an executable file, complete source code means
all the source code for all modules it contains; but, as a special
exception, it need not include source code for modules which are standard
libraries that accompany the operating system on which the executable
file runs, or for standard header files or definitions files that
accompany that operating system.

  4. You may not copy, modify, sublicense, distribute or transfer the
Program except as expressly provided under this General Public License.
Any attempt otherwise to copy, modify, sublicense, distribute or transfer
the Program is void, and will automatically terminate your rights to use
the Program under this License.  However, parties who have received
copies, or rights to use copies, from you under this General Public
License will not have their licenses terminated so long as such parties
remain in full compliance.

  5. By copying, distributing or modifying the Program (or any work based
on the Program) you indicate your acceptance of this license to do so,
and all its terms and conditions.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the original
licensor to copy, distribute or modify the Program subject to these
terms and conditions.  You may not impose any further restrictions on the
recipients' exercise of the rights granted herein.

  7. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of the license which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
the license, you may choose any version ever published by the Free Software
Foundation.

  8. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

        Appendix: How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to humanity, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.

  To do so, attach the following notices to the program.  It is safest to
attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.

    
    Copyright (C) 19yy  

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 1, or (at your option)
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA  02110-1301 USA


Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) 19xx name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the
appropriate parts of the General Public License.  Of course, the
commands you use may be called something other than `show w' and `show
c'; they could even be mouse-clicks or menu items--whatever suits your
program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the
  program `Gnomovision' (a program to direct compilers to make passes
  at assemblers) written by James Hacker.

  , 1 April 1989
  Ty Coon, President of Vice

That's all there is to it!


--- The Artistic License 1.0 ---

This is free software, licensed under:

  The Artistic License 1.0

The Artistic License

Preamble

The intent of this document is to state the conditions under which a Package
may be copied, such that the Copyright Holder maintains some semblance of
artistic control over the development of the package, while giving the users of
the package the right to use and distribute the Package in a more-or-less
customary fashion, plus the right to make reasonable modifications.

Definitions:

  - "Package" refers to the collection of files distributed by the Copyright
    Holder, and derivatives of that collection of files created through
    textual modification. 
  - "Standard Version" refers to such a Package if it has not been modified,
    or has been modified in accordance with the wishes of the Copyright
    Holder. 
  - "Copyright Holder" is whoever is named in the copyright or copyrights for
    the package. 
  - "You" is you, if you're thinking about copying or distributing this Package.
  - "Reasonable copying fee" is whatever you can justify on the basis of media
    cost, duplication charges, time of people involved, and so on. (You will
    not be required to justify it to the Copyright Holder, but only to the
    computing community at large as a market that must bear the fee.) 
  - "Freely Available" means that no fee is charged for the item itself, though
    there may be fees involved in handling the item. It also means that
    recipients of the item may redistribute it under the same conditions they
    received it. 

1. You may make and give away verbatim copies of the source form of the
Standard Version of this Package without restriction, provided that you
duplicate all of the original copyright notices and associated disclaimers.

2. You may apply bug fixes, portability fixes and other modifications derived
from the Public Domain or from the Copyright Holder. A Package modified in such
a way shall still be considered the Standard Version.

3. You may otherwise modify your copy of this Package in any way, provided that
you insert a prominent notice in each changed file stating how and when you
changed that file, and provided that you do at least ONE of the following:

  a) place your modifications in the Public Domain or otherwise make them
     Freely Available, such as by posting said modifications to Usenet or an
     equivalent medium, or placing the modifications on a major archive site
     such as ftp.uu.net, or by allowing the Copyright Holder to include your
     modifications in the Standard Version of the Package.

  b) use the modified Package only within your corporation or organization.

  c) rename any non-standard executables so the names do not conflict with
     standard executables, which must also be provided, and provide a separate
     manual page for each non-standard executable that clearly documents how it
     differs from the Standard Version.

  d) make other distribution arrangements with the Copyright Holder.

4. You may distribute the programs of this Package in object code or executable
form, provided that you do at least ONE of the following:

  a) distribute a Standard Version of the executables and library files,
     together with instructions (in the manual page or equivalent) on where to
     get the Standard Version.

  b) accompany the distribution with the machine-readable source of the Package
     with your modifications.

  c) accompany any non-standard executables with their corresponding Standard
     Version executables, giving the non-standard executables non-standard
     names, and clearly documenting the differences in manual pages (or
     equivalent), together with instructions on where to get the Standard
     Version.

  d) make other distribution arrangements with the Copyright Holder.

5. You may charge a reasonable copying fee for any distribution of this
Package.  You may charge any fee you choose for support of this Package. You
may not charge a fee for this Package itself. However, you may distribute this
Package in aggregate with other (possibly commercial) programs as part of a
larger (possibly commercial) software distribution provided that you do not
advertise this Package as a product of your own.

6. The scripts and library files supplied as input to or produced as output
from the programs of this Package do not automatically fall under the copyright
of this Package, but belong to whomever generated them, and may be sold
commercially, and may be aggregated with this Package.

7. C or perl subroutines supplied by you and linked into this Package shall not
be considered part of this Package.

8. The name of the Copyright Holder may not be used to endorse or promote
products derived from this software without specific prior written permission.

9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

The End

Finance-QuoteHist-1.32/MANIFEST0000644000175000017500000000140314021707227014562 0ustar  sisksiskREADME
Changes
LICENSE
MANIFEST
Makefile.PL
lib/Finance/QuoteHist.pm
lib/Finance/QuoteHist/Generic.pm
lib/Finance/QuoteHist/Yahoo.pm
lib/Finance/QuoteHist/Google.pm
lib/Finance/QuoteHist/DailyFinance.pm
t/00_basic.t
t/05_csv.t
t/10_quotes.t
t/20_dividends.t
t/30_splits.t
t/testload.pm
t/dat/csv.dat
t/dat/quote_daily_dailyfinance.dat
t/dat/quote_daily_google.dat
t/dat/quote_daily_plain.dat
t/dat/quote_daily_yahoo.dat
t/dat/quote_weekly_google.dat
t/dat/quote_weekly_yahoo.dat
t/dat/quote_monthly_yahoo.dat
t/dat/dividend_yahoo.dat
t/dat/dividend_plain.dat
t/dat/split_plain.dat
t/dat/split_yahoo.dat
META.yml                                 Module YAML meta-data (added by MakeMaker)
META.json                                Module JSON meta-data (added by MakeMaker)
Finance-QuoteHist-1.32/README0000644000175000017500000000536612453534725014335 0ustar  sisksiskFinance-QuoteHist 
-----------------

The Finance-QuoteHist bundle is several modules designed to fetch
historical stock quotes from the web: 
  
Finance::QuoteHist
------------------
Top level aggregator that will select a default lineup of
site instances from which to retrieve quotes. Other
than the default lineup, this module behaves identically
to whichever site-specific module is first in the lineup.
See below for site-specific modules. 

Finance::QuoteHist::Generic
---------------------------
The real workhorse of the bundle, a sub class of
LWP::UserAgent. Site-specific modules are sub classes
of this module.


INSTALLATION

You install HTML-Element-Extended, as you would install any
perl library, by running these commands:

   perl Makefile.PL
   make
   make test
   make install


DOCUMENTATION

POD style documentation is included with each module. This is normally
converted to a manual page and installed as part of the "make install"
process. You should also be able to use the 'perldoc' utility to
extract and read documentation from the module file directly.  See
Changes for recent changes.


SUPPORT

The source code is tracked on GitHub:

https://github.com/mojotoad/Finance-QuoteHist

Problems can be reported there. Questions and comments can also be
directed to Matt Sisk 

AVAILABILITY

In addition to GitHub, the library is available from CPAN:

   http://www.cpan.org/authors/id/M/MS/MSISK/


DISCLAIMER 

The data returned from these modules is in no way guaranteed, nor
are the developers responsible in any way for how this data (or lack
thereof) is used. The interface is based on URLs and page layouts
that might change at any time. Even though these modules are designed
to be adaptive under these circumstances, they will at some point
probably be unable to retrieve data unless fixed or provided with new
parameters. Furthermore, the data from these web sites is usually not
even guaranteed by the web sites themselves, and oftentimes is
acquired elsewhere. See the documentation for each site-specific
module for more information regarding the disclaimer for that site. 


ACKNOWLEDGEMENTS

Thanks to Jim Miner for his generous efforts testing this module. Thanks
to Josh Woodward for some pointers on the Yahoo! interface. Thanks to
Jacob Anawalt for pointers on the Yahoo! interface and suggestions on
open-ended date queries. Thanks to Paul McDermott and Jay Strauss for
patches. Thanks to Manoj Bhatti, Bernd Hacker, Christian von
Engelbrechten, Gary Nielson, Mike Brown, Bill Stephenson, and Robert S.
Weigel for their bug-spotting.


COPYRIGHT

Copyright (c) 1999-2013 Matthew P. Sisk.  All rights reserved.
All wrongs revenged. This program is free software; you can
redistribute it and/or modify it under the same terms as
Perl itself.
Finance-QuoteHist-1.32/META.json0000664000175000017500000000211014021707227015050 0ustar  sisksisk{
   "abstract" : "unknown",
   "author" : [
      "unknown"
   ],
   "dynamic_config" : 1,
   "generated_by" : "ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010",
   "license" : [
      "unknown"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
      "version" : 2
   },
   "name" : "Finance-QuoteHist",
   "no_index" : {
      "directory" : [
         "t",
         "inc"
      ]
   },
   "prereqs" : {
      "build" : {
         "requires" : {
            "ExtUtils::MakeMaker" : "0"
         }
      },
      "configure" : {
         "requires" : {
            "ExtUtils::MakeMaker" : "0"
         }
      },
      "runtime" : {
         "requires" : {
            "Date::Manip" : "0",
            "HTML::TableExtract" : "2.07",
            "HTTP::Request" : "0",
            "JSON" : "0",
            "LWP::UserAgent" : "0",
            "MIME::Base64" : "0",
            "Text::CSV" : "0"
         }
      }
   },
   "release_status" : "stable",
   "version" : "1.32",
   "x_serialization_backend" : "JSON::PP version 4.02"
}
Finance-QuoteHist-1.32/t/0000755000175000017500000000000014021707227013676 5ustar  sisksiskFinance-QuoteHist-1.32/t/10_quotes.t0000644000175000017500000000233713554076261015717 0ustar  sisksiskuse FindBin;
use lib $FindBin::RealBin;
use testload;

my $tcount;
BEGIN { $tcount = 8 }
use Test::More tests => $tcount;

use FindBin;
use lib $FindBin::RealBin;
use testload;

SKIP: {
  skip("quotes (no connect)", $tcount) unless network_ok();
  for my $src (sources()) {
    for my $gran (granularities($src)) {
      SKIP: {
        skip("(dev only) $src-$gran test", 2)
          unless DEV_TESTS || $src eq GOLDEN_CHILD;
        my($m, $sym, $start, $end, $dat) = basis($src, 'quote', $gran);
        next unless $m;
        eval "use $m";
        my %parms = ( class => $m, granularity => $gran );
        quote_cmp(
          $sym, $start, $end,
          "direct quotes ($src:$gran)",
          $dat, %parms
        );
      }
    }
  }
}

sub quote_cmp {
  @_ >= 5 or die "Problem with args\n";
  my($symbol, $start_date, $end_date, $label, $dat, %parms) = @_;
  my $q = new_quotehist($symbol, $start_date, $end_date, %parms);
  my @rows = $q->quotes;
  cmp_ok(scalar @rows, '==', scalar @$dat, "$label (rows)");
  for my $i (0 .. $#rows) {
    # drop adjusted and volume, too variable for testing
    pop @{$rows[$i]} while @{$rows[$i]} > 6;
    $rows[$i] = join(':', @{$rows[$i]});
  }
  is_deeply(\@rows, $dat, "$label (content)");
}
Finance-QuoteHist-1.32/t/05_csv.t0000644000175000017500000000254512453534725015200 0ustar  sisksiskmy $test_count;
BEGIN { $test_count = 161 }

use strict;
use Test::More tests => $test_count;

use FindBin;
use lib $FindBin::RealBin;
use testload;

my @headers = ( 'Date', 'Open', 'High', 'Low', 'Close',
                'Volume', 'Adj. Close*');

my($pp_present, $xs_present);
eval  { require Text::CSV_PP };
$pp_present = !$@;
eval  { require Text::CSV_XS };
$xs_present = !$@;

my $each_count = ($test_count - 1)/2;

SKIP: {
  my $class = 'Text::CSV_PP';
  skip "$class not installed",  $each_count unless $pp_present;
  use_ok($class);
  my $cp = $class->new;
  csv_parse($cp, csv_content());
}

SKIP: {
  my $class = 'Text::CSV_XS';
  skip "$class not installed",  $each_count unless $xs_present;
  use_ok($class);
  my $cp = $class->new;
  csv_parse($cp, csv_content());
}

ok($pp_present || $xs_present, "csv parsing class present");

###

sub csv_parse {
  my($cp, $str) = @_;
  my @rows = split(/\s*\n\s*/, $str);
  chomp @rows;
  my $first_line = shift @rows;
  ok($cp->parse($first_line), "header parse");
  my @fields = $cp->fields;
  cmp_ok(scalar @fields, '==', scalar @headers,  "header field count");
  foreach (0 .. $#headers) {
    cmp_ok($fields[$_], 'eq', $headers[$_], "header field match");
  }
  foreach my $line (@rows) {
    ok($cp->parse($line), 'line parse');
    my @fields = $cp->fields;
    cmp_ok(scalar @fields, '==', 7, "line field count");
  }
}
Finance-QuoteHist-1.32/t/dat/0000755000175000017500000000000014021707227014446 5ustar  sisksiskFinance-QuoteHist-1.32/t/dat/quote_monthly_yahoo.dat0000644000175000017500000000166613554074437021271 0ustar  sisksiskFinance::QuoteHist::Yahoo
IBM,2012/07/01,2013/07/01
IBM:2012/07/01:196.3600:197.8400:181.8500:195.9800:91516500:151.7178
IBM:2012/08/01:196.9600:202.0000:193.0200:194.8500:60653700:150.8430
IBM:2012/09/01:196.6100:208.3200:193.2500:207.4500:77341400:161.2830
IBM:2012/10/01:208.0100:211.7900:190.5600:194.5300:96155000:151.2383
IBM:2012/11/01:194.6800:198.0000:184.7800:190.0700:81892500:147.7708
IBM:2012/12/01:190.7600:196.4500:186.9400:191.5500:79320200:149.5732
IBM:2013/01/01:194.0900:208.5800:190.3900:203.0700:87636600:158.5687
IBM:2013/02/01:204.6500:205.3500:197.5100:200.8300:64144100:156.8195
IBM:2013/03/01:200.6500:215.9000:199.3600:213.3000:76023800:167.2579
IBM:2013/04/01:212.8000:214.8900:187.6800:202.5400:108666600:158.8206
IBM:2013/05/01:201.8700:211.9800:199.2000:208.0200:90683600:163.1176
IBM:2013/06/01:208.2500:210.0500:188.4100:191.1100:80215000:150.5602
IBM:2013/07/01:192.1500:200.9400:190.2600:195.0400:88079400:153.6563
Finance-QuoteHist-1.32/t/dat/dividend_plain.dat0000644000175000017500000000043413554074432020117 0ustar  sisksiskFinance::QuoteHist
INTC,2011/01/01,2013/10/01
INTC:2011/02/03:0.181
INTC:2011/05/04:0.181
INTC:2011/08/03:0.21
INTC:2011/11/03:0.21
INTC:2012/02/03:0.21
INTC:2012/05/03:0.21
INTC:2012/08/03:0.225
INTC:2012/11/05:0.225
INTC:2013/02/05:0.225
INTC:2013/05/03:0.225
INTC:2013/08/05:0.225
Finance-QuoteHist-1.32/t/dat/quote_daily_yahoo.dat0000644000175000017500000004124213554074434020670 0ustar  sisksiskFinance::QuoteHist::Yahoo
IBM,2012/07/01,2013/07/01
IBM:2012/07/02:196.3600:197.2000:194.8500:195.8300:2827000:151.6017
IBM:2012/07/03:195.4600:196.3400:194.9100:195.9300:1450400:151.6791
IBM:2012/07/05:194.8800:196.8500:193.6300:195.2900:2690200:151.1836
IBM:2012/07/06:193.9200:193.9400:189.7400:191.4100:4952900:148.1799
IBM:2012/07/09:190.7600:191.0000:188.0500:189.6700:3988100:146.8329
IBM:2012/07/10:190.3000:191.1400:185.6000:186.2600:4690300:144.1931
IBM:2012/07/11:186.2200:187.3600:183.5100:185.2500:5456100:143.4112
IBM:2012/07/12:184.2500:184.3900:181.8500:183.0900:4931300:141.7390
IBM:2012/07/13:183.4600:186.3300:183.0300:186.0100:3933000:143.9995
IBM:2012/07/16:185.5800:186.1000:184.5800:184.7900:3144400:143.0551
IBM:2012/07/17:185.7300:186.2900:183.2000:183.6500:5158600:142.1725
IBM:2012/07/18:184.1500:188.5900:183.5500:188.2500:8019500:145.7336
IBM:2012/07/19:193.4000:196.8500:192.9700:195.3400:10395400:151.2224
IBM:2012/07/20:194.0900:194.9000:192.1700:192.4500:4789700:148.9851
IBM:2012/07/23:189.7800:191.3000:188.2000:190.8300:3904500:147.7309
IBM:2012/07/24:190.9200:191.3200:188.5600:190.3400:3597100:147.3516
IBM:2012/07/25:190.3100:192.7700:189.3200:191.0800:3833800:147.9244
IBM:2012/07/26:193.4900:194.9500:192.5700:193.9500:3282900:150.1463
IBM:2012/07/27:195.1000:197.4100:193.9500:196.3900:4177300:152.0352
IBM:2012/07/30:196.3200:197.8400:195.9200:196.6800:2787000:152.2597
IBM:2012/07/31:196.5000:197.5800:195.8800:195.9800:3507000:151.7178
IBM:2012/08/01:196.9600:197.8500:194.7200:195.1800:2559300:151.0985
IBM:2012/08/02:194.1600:196.6000:193.0200:194.4500:2812600:150.5334
IBM:2012/08/03:196.4800:198.9500:196.1600:198.5200:3278100:153.6842
IBM:2012/08/06:198.7600:199.9400:198.5200:198.7600:2337800:153.8699
IBM:2012/08/07:199.4300:200.8800:198.8000:199.9300:3209400:154.7757
IBM:2012/08/08:198.2700:199.6900:198.1600:199.0300:2068300:154.7368
IBM:2012/08/09:198.6200:199.4600:197.8900:198.4200:2160100:154.2626
IBM:2012/08/10:197.8700:199.3700:197.2400:199.2900:2600500:154.9389
IBM:2012/08/13:198.8800:199.9800:197.7900:199.0100:2418300:154.7213
IBM:2012/08/14:198.8800:199.3300:197.7200:198.2900:2562300:154.1615
IBM:2012/08/15:198.9000:199.3300:197.9200:198.4000:2111800:154.2470
IBM:2012/08/16:198.7700:201.3200:198.1200:200.8400:2729500:156.1441
IBM:2012/08/17:201.0800:202.0000:200.6600:201.2200:2551800:156.4395
IBM:2012/08/20:200.6900:201.1300:200.0300:200.5000:2336000:155.8796
IBM:2012/08/21:200.9900:201.0000:198.3700:198.6500:3111300:154.4414
IBM:2012/08/22:198.6400:198.6500:196.9000:197.2500:3296100:153.3530
IBM:2012/08/23:197.0400:197.3700:195.4400:195.7000:2757400:152.1479
IBM:2012/08/24:194.9600:198.1100:194.2000:197.7700:2639500:153.7573
IBM:2012/08/27:197.9600:198.3000:195.6100:195.6900:2498800:152.1401
IBM:2012/08/28:195.5600:196.1100:194.5000:194.8700:2539200:151.5026
IBM:2012/08/29:195.1300:196.0400:194.9000:195.0800:2141400:151.6659
IBM:2012/08/30:194.7700:195.4700:193.1800:193.3700:2740900:150.3364
IBM:2012/08/31:194.3100:195.9500:193.4600:194.8500:3193300:151.4870
IBM:2012/09/04:196.6100:197.1700:193.2500:194.5400:4514400:151.2460
IBM:2012/09/05:194.4100:195.8500:193.8900:195.0400:3312500:151.6348
IBM:2012/09/06:196.2600:199.4600:196.1100:199.1000:3931700:154.7913
IBM:2012/09/07:199.1200:199.5000:198.0800:199.5000:3413700:155.1022
IBM:2012/09/10:199.3900:201.8200:198.7300:200.9500:4208000:156.2295
IBM:2012/09/11:200.5500:203.4600:200.5100:203.2700:3910600:158.0332
IBM:2012/09/12:203.5200:204.6500:202.9600:203.7700:3284000:158.4219
IBM:2012/09/13:204.3800:206.8100:203.4600:206.3600:3879600:160.4356
IBM:2012/09/14:206.2400:207.6500:206.0500:206.8100:4057100:160.7854
IBM:2012/09/17:205.5500:207.9900:205.5500:207.1500:3275100:161.0498
IBM:2012/09/18:206.8800:207.8800:206.4800:207.0700:2476100:160.9875
IBM:2012/09/19:207.5500:207.5700:206.0100:206.4300:2605600:160.4900
IBM:2012/09/20:205.8500:206.9900:205.3000:206.1800:3739100:160.2956
IBM:2012/09/21:207.3600:207.9400:205.9200:205.9800:12771500:160.1401
IBM:2012/09/24:205.0200:206.0600:204.9000:205.2900:3511100:159.6037
IBM:2012/09/25:205.6000:207.3200:204.9500:204.9800:3345200:159.3627
IBM:2012/09/26:205.0500:205.9800:203.9000:204.0000:3423300:158.6008
IBM:2012/09/27:204.3700:206.9000:204.1400:205.9100:2771900:160.0857
IBM:2012/09/28:205.6600:208.3200:205.3100:207.4500:4910900:161.2830
IBM:2012/10/01:208.0100:211.7500:207.9400:210.4700:5456900:163.6309
IBM:2012/10/02:210.9600:211.4700:208.5000:209.8400:3321000:163.1411
IBM:2012/10/03:209.9400:211.3000:209.3000:210.5100:3192300:163.6620
IBM:2012/10/04:210.2800:210.9700:208.5300:210.3900:2957500:163.5687
IBM:2012/10/05:211.1500:211.7900:210.0600:210.5900:2858400:163.7242
IBM:2012/10/08:210.0200:210.6400:209.2100:209.8200:1982600:163.1255
IBM:2012/10/09:209.9500:210.7400:207.7000:207.9900:3580400:161.7029
IBM:2012/10/10:207.8200:208.2300:205.0500:205.8200:4024500:160.0157
IBM:2012/10/11:206.5700:207.2700:205.6300:205.7600:2910000:159.9691
IBM:2012/10/12:205.6400:207.9600:205.5600:207.8000:2712000:161.5551
IBM:2012/10/15:208.8800:209.4000:207.8200:208.9300:3060900:162.4336
IBM:2012/10/16:209.9400:211.0000:209.4700:211.0000:5632100:164.0430
IBM:2012/10/17:201.2100:203.4100:198.5500:200.6300:12673900:155.9807
IBM:2012/10/18:199.9100:200.1400:194.0700:194.9600:9295900:151.5726
IBM:2012/10/19:195.2900:196.0800:193.1800:193.3600:6620300:150.3287
IBM:2012/10/22:193.7100:194.6800:192.4400:194.4000:4402500:151.1372
IBM:2012/10/23:193.5300:194.0000:190.9400:191.2500:4427400:148.6882
IBM:2012/10/24:192.4400:193.2900:190.5600:190.7200:3738000:148.2762
IBM:2012/10/25:191.4100:192.4400:190.7100:191.6000:3249600:148.9603
IBM:2012/10/26:191.4900:193.8700:191.4700:193.2700:4006500:150.2587
IBM:2012/10/31:194.8000:196.4100:193.6300:194.5300:6052300:151.2383
IBM:2012/11/01:194.6800:197.8900:194.5500:197.1500:3758800:153.2752
IBM:2012/11/02:197.5300:198.0000:193.2900:193.4300:4260100:150.3831
IBM:2012/11/05:192.3600:194.9700:192.0100:194.1400:2736200:150.9351
IBM:2012/11/06:195.2600:196.7500:194.6800:195.0700:3281000:151.6581
IBM:2012/11/07:193.3600:193.6800:191.1600:191.1600:4746200:149.2687
IBM:2012/11/08:191.4000:191.8100:190.0900:190.1000:3542200:148.4410
IBM:2012/11/09:189.8500:191.6400:188.8800:189.6400:3671600:148.0817
IBM:2012/11/12:190.0300:190.7700:187.4100:189.2500:3067400:147.7772
IBM:2012/11/13:188.3900:190.8800:188.2500:188.3200:3406700:147.0510
IBM:2012/11/14:189.1400:189.2700:185.2800:185.5100:4404400:144.8568
IBM:2012/11/15:185.8300:187.2000:184.7800:185.8500:3411700:145.1223
IBM:2012/11/16:186.3300:187.5600:185.1000:186.9400:4660800:145.9735
IBM:2012/11/19:188.6200:190.3900:188.3000:190.3500:3588900:148.6362
IBM:2012/11/20:189.8800:190.1100:188.2500:189.2000:3170200:147.7382
IBM:2012/11/21:189.3000:190.8900:188.9500:190.2900:5189600:148.5893
IBM:2012/11/23:191.0000:193.4900:190.8000:193.4900:3877600:151.0881
IBM:2012/11/26:192.4500:193.3700:191.7700:192.8800:5210300:150.6118
IBM:2012/11/27:192.0800:193.0500:191.0000:191.2300:3290900:149.3233
IBM:2012/11/28:190.9800:192.0400:189.2700:191.9800:3603600:149.9090
IBM:2012/11/29:192.7500:192.9000:190.2000:191.5300:4077900:149.5576
IBM:2012/11/30:191.7500:192.0000:189.5000:190.0700:4936400:148.4175
IBM:2012/12/03:190.7600:191.3000:188.3600:189.4800:3349600:147.9569
IBM:2012/12/04:189.7100:190.7500:189.0200:189.3600:3990700:147.8632
IBM:2012/12/05:189.0100:189.4600:186.9400:188.6500:4202100:147.3087
IBM:2012/12/06:189.1700:190.1800:188.4100:189.7000:3216600:148.1286
IBM:2012/12/07:190.1400:192.2000:190.1100:191.9500:4091300:149.8856
IBM:2012/12/10:192.1700:193.5400:191.6500:192.6200:3438500:150.4087
IBM:2012/12/11:193.1800:194.8000:193.1500:194.2000:4144000:151.6425
IBM:2012/12/12:194.6000:194.7500:192.4600:192.9500:4241900:150.6664
IBM:2012/12/13:192.1800:193.0500:191.3700:191.9900:3623200:149.9168
IBM:2012/12/14:191.6900:193.4000:191.2600:191.7600:3826900:149.7372
IBM:2012/12/17:191.7600:193.9100:191.7600:193.6200:3792400:151.1896
IBM:2012/12/18:193.8500:195.9200:193.8400:195.6900:4125900:152.8060
IBM:2012/12/19:195.4600:196.4500:195.0000:195.0800:4270500:152.3296
IBM:2012/12/20:194.4700:194.9500:193.1600:194.7700:4184500:152.0876
IBM:2012/12/21:193.1000:194.5000:191.5700:193.4200:8225300:151.0334
IBM:2012/12/24:193.0500:193.8300:192.0200:192.4000:1631200:150.2370
IBM:2012/12/26:192.2100:193.5300:190.9500:191.9500:2776500:149.8856
IBM:2012/12/27:191.5300:193.2800:190.7000:192.7100:3967600:150.4790
IBM:2012/12/28:191.1100:192.2200:189.8300:189.8300:3536600:148.2301
IBM:2012/12/31:189.2300:191.6800:188.8400:191.5500:4684900:149.5732
IBM:2013/01/02:194.0900:196.3500:193.8000:196.3500:4234100:153.3213
IBM:2013/01/03:195.6700:196.2900:194.4400:195.2700:3644700:152.4780
IBM:2013/01/04:194.1900:194.4600:192.7800:193.9900:3380200:151.4785
IBM:2013/01/07:193.4000:193.7800:192.3400:193.1400:2862300:150.8148
IBM:2013/01/08:192.9200:193.3000:191.6000:192.8700:3026900:150.6040
IBM:2013/01/09:193.4800:193.4900:191.6500:192.3200:3212000:150.1745
IBM:2013/01/10:192.6500:192.9600:191.2800:192.8800:3608100:150.6118
IBM:2013/01/11:194.1500:195.0000:192.9000:194.4500:3880400:151.8377
IBM:2013/01/14:192.8200:193.2800:191.7500:192.6200:4172200:150.4087
IBM:2013/01/15:191.3100:192.7300:190.3900:192.5000:4172100:150.3150
IBM:2013/01/16:192.0000:193.1800:191.3500:192.5900:2962900:150.3853
IBM:2013/01/17:193.8500:194.4600:193.2400:193.6500:3881600:151.2130
IBM:2013/01/18:194.0300:195.0000:193.8000:194.4700:4559600:151.8533
IBM:2013/01/22:194.3600:196.0800:194.0100:196.0800:7172300:153.1105
IBM:2013/01/23:203.5000:208.5800:203.3600:204.7200:12530500:159.8571
IBM:2013/01/24:203.9100:205.0600:203.0800:204.4200:4447200:159.6228
IBM:2013/01/25:204.4500:205.1800:204.1300:204.9700:3358900:160.0523
IBM:2013/01/28:204.8500:206.2200:204.2900:204.9300:2823700:160.0211
IBM:2013/01/29:204.3400:205.7300:203.6400:203.9000:3617100:159.2167
IBM:2013/01/30:203.6900:204.8800:203.1900:203.5200:2998400:158.9201
IBM:2013/01/31:203.3200:204.4700:202.9600:203.0700:3091400:158.5687
IBM:2013/02/01:204.6500:205.3500:203.8400:205.1800:3370700:160.2163
IBM:2013/02/04:204.1900:205.0200:203.5700:203.7900:3188800:159.1309
IBM:2013/02/05:204.3100:204.7500:202.5100:202.7900:3636900:158.3501
IBM:2013/02/06:200.3900:201.2900:199.5600:201.0200:3624200:157.6286
IBM:2013/02/07:200.6200:200.9100:198.6800:199.7400:3076700:156.6249
IBM:2013/02/08:199.9700:202.0900:199.6800:201.6800:2893300:158.1461
IBM:2013/02/11:200.9800:201.9500:199.7500:200.1600:2944700:156.9543
IBM:2013/02/12:200.0100:200.7400:199.0200:200.0400:2461800:156.8602
IBM:2013/02/13:200.6500:200.9500:199.5700:200.0900:2169800:156.8994
IBM:2013/02/14:199.7300:200.3200:199.2600:199.6500:3294200:156.5544
IBM:2013/02/15:199.9800:201.2500:199.8200:200.9800:3627800:157.5973
IBM:2013/02/19:200.6000:201.8900:200.2200:200.3200:2998300:157.0797
IBM:2013/02/20:200.6200:201.7200:198.8600:199.3100:3715400:156.2877
IBM:2013/02/21:198.6300:199.0700:198.1100:198.3300:3922900:155.5193
IBM:2013/02/22:199.2300:201.0900:198.8400:201.0900:3107900:157.6835
IBM:2013/02/25:201.6700:202.4900:197.5100:197.5100:3844800:154.8763
IBM:2013/02/26:198.6300:199.9000:197.8400:199.1400:3391600:156.1544
IBM:2013/02/27:198.8900:202.7500:198.6000:202.3300:4185100:158.6559
IBM:2013/02/28:202.1800:203.1200:200.7900:200.8300:4689200:157.4796
IBM:2013/03/01:200.6500:202.9400:199.3600:202.9100:3308300:159.1106
IBM:2013/03/04:202.5900:205.1900:202.5500:205.1900:3693400:160.8985
IBM:2013/03/05:205.8600:207.7000:205.6900:206.5300:3807800:161.9493
IBM:2013/03/06:207.0300:208.4900:206.6600:208.3800:3594800:163.3999
IBM:2013/03/07:208.2900:209.6000:208.2400:209.4200:3884400:164.2154
IBM:2013/03/08:209.8500:210.7400:209.4300:210.3800:3700500:164.9682
IBM:2013/03/11:210.0400:210.2000:209.0400:210.0800:3049700:164.7330
IBM:2013/03/12:209.4000:210.7300:209.0900:210.5500:3591600:165.1015
IBM:2013/03/13:210.2000:212.3600:209.7700:212.0600:3355900:166.2856
IBM:2013/03/14:212.1500:215.8600:212.1500:215.8000:5505500:169.2183
IBM:2013/03/15:215.3800:215.9000:213.4100:214.9200:7930300:168.5283
IBM:2013/03/18:212.9000:214.5000:212.6400:213.2100:3006000:167.1874
IBM:2013/03/19:214.1300:215.1200:211.8300:213.4400:3198600:167.3677
IBM:2013/03/20:214.7600:215.8200:214.3000:215.0600:3019000:168.6380
IBM:2013/03/21:212.9600:213.0000:210.1100:212.2600:5830400:166.4424
IBM:2013/03/22:212.2100:213.1700:211.6200:212.0800:3031500:166.3013
IBM:2013/03/25:212.5400:212.8100:210.0500:210.7400:3242500:165.2505
IBM:2013/03/26:211.7700:212.5000:211.5000:212.3600:2300300:166.5208
IBM:2013/03/27:210.9600:212.1600:210.1000:210.8900:3223400:165.3681
IBM:2013/03/28:209.8300:213.4400:209.7400:213.3000:3749900:167.2579
IBM:2013/04/01:212.8000:213.5000:211.2500:212.3800:2144500:166.5365
IBM:2013/04/02:212.9200:214.7100:211.5700:214.3600:3116800:168.0891
IBM:2013/04/03:214.3200:214.8900:212.6300:212.6600:3128800:166.7561
IBM:2013/04/04:212.7700:212.9300:210.0400:211.3100:3709500:165.6975
IBM:2013/04/05:209.1000:209.8400:206.3400:209.4100:4148000:164.2076
IBM:2013/04/08:209.0700:209.3200:207.3300:209.3200:2351200:164.1370
IBM:2013/04/09:209.5600:210.0000:208.6100:209.2200:2577300:164.0587
IBM:2013/04/10:211.8600:212.5100:210.4700:212.0000:3524200:166.2385
IBM:2013/04/11:211.5000:213.0900:210.8500:212.9200:3655600:166.9599
IBM:2013/04/12:210.7900:211.4900:209.7800:211.3800:3165400:165.7524
IBM:2013/04/15:210.4400:211.6900:209.2600:209.2600:4218100:164.0900
IBM:2013/04/16:210.8400:212.0000:209.2000:212.0000:2859500:166.2385
IBM:2013/04/17:210.5300:211.0900:209.5000:209.6700:3269800:164.4115
IBM:2013/04/18:210.1100:210.2500:206.1500:207.1500:6502000:162.4354
IBM:2013/04/19:195.7400:196.5000:189.7600:190.0000:18847000:148.9874
IBM:2013/04/22:191.1500:191.3800:187.6800:187.8300:9845400:147.2858
IBM:2013/04/23:189.2700:192.5500:188.6100:191.6100:6296100:150.2498
IBM:2013/04/24:192.1600:192.5000:191.0900:191.7100:3693900:150.3283
IBM:2013/04/25:192.6900:195.1700:192.3200:193.9500:4650900:152.0847
IBM:2013/04/26:194.1600:194.7100:193.2500:194.3100:3490300:152.3671
IBM:2013/04/29:194.7800:199.6800:194.6500:199.1500:5932400:156.1623
IBM:2013/04/30:199.1300:202.5900:197.3500:202.5400:7539900:158.8206
IBM:2013/05/01:201.8700:202.1700:199.2000:199.6300:4899100:156.5387
IBM:2013/05/02:200.1200:202.4300:199.7500:202.3900:3945100:158.7029
IBM:2013/05/03:203.9400:205.3200:203.7500:204.5100:4492700:160.3653
IBM:2013/05/06:203.7900:203.9900:201.5200:202.7800:4826500:159.0087
IBM:2013/05/07:202.8100:203.7300:201.6500:203.6300:3451000:159.6753
IBM:2013/05/08:202.9400:204.8500:202.5100:204.8200:3601700:161.3612
IBM:2013/05/09:204.6900:205.0000:202.7200:203.2400:3542300:160.1164
IBM:2013/05/10:203.3700:204.5300:202.8200:204.4700:3279200:161.0855
IBM:2013/05/13:204.1800:204.4700:202.2200:202.4700:3648400:159.5098
IBM:2013/05/14:202.0900:203.6700:202.0800:203.2100:3699700:160.0928
IBM:2013/05/15:202.2500:203.6800:202.0400:203.3200:4028100:160.1795
IBM:2013/05/16:204.0000:206.5900:204.0000:204.6900:4507000:161.2587
IBM:2013/05/17:205.2500:209.5000:204.9900:208.4400:5704100:164.2131
IBM:2013/05/20:208.0200:209.1500:207.4200:207.6000:3614500:163.5513
IBM:2013/05/21:207.2400:209.4800:207.0000:208.6500:2982900:164.3785
IBM:2013/05/22:208.5000:210.1500:206.1200:206.9900:5186800:163.0707
IBM:2013/05/23:205.4800:208.6100:205.1300:206.1600:5179100:162.4169
IBM:2013/05/24:204.6900:206.4300:204.4200:205.7200:3295800:162.0702
IBM:2013/05/28:207.4400:208.5900:207.1600:207.7800:4011500:163.6931
IBM:2013/05/29:206.2200:208.4200:206.0400:207.9200:3439400:163.8034
IBM:2013/05/30:206.9700:210.9900:206.9700:209.3600:4399700:164.9379
IBM:2013/05/31:208.5900:211.9800:208.0200:208.0200:4949000:163.8822
IBM:2013/06/03:208.2500:210.0500:207.3300:208.9500:3322000:164.6149
IBM:2013/06/04:208.6000:209.0000:205.0800:206.1900:3598300:162.4405
IBM:2013/06/05:205.1600:206.2900:202.4000:202.7400:4215400:159.7225
IBM:2013/06/06:203.0000:204.2000:201.4700:203.8000:3679400:160.5576
IBM:2013/06/07:204.8500:206.3500:204.1100:206.3500:2953400:162.5666
IBM:2013/06/10:206.9700:206.9800:204.9000:205.0200:2652300:161.5188
IBM:2013/06/11:203.1700:205.9500:202.5100:203.9800:2814400:160.6994
IBM:2013/06/12:204.5900:205.2300:200.5100:201.2000:3409800:158.5093
IBM:2013/06/13:201.0100:204.4000:200.3400:203.7700:2899500:160.5339
IBM:2013/06/14:203.9700:204.7400:201.8100:202.2000:2804500:159.2971
IBM:2013/06/17:203.4400:205.1700:202.5500:203.0400:3219900:159.9588
IBM:2013/06/18:203.0200:206.0900:202.8700:204.8700:3277800:161.4006
IBM:2013/06/19:204.4400:205.0300:201.9300:201.9400:2846100:159.0923
IBM:2013/06/20:200.6700:201.7000:197.2800:197.3500:4514800:155.4762
IBM:2013/06/21:198.5000:198.5200:193.5400:195.4600:8914800:153.9872
IBM:2013/06/24:193.9900:194.9100:191.3400:193.5400:4356200:152.4746
IBM:2013/06/25:195.3100:195.5900:193.1500:194.9800:3846200:153.6090
IBM:2013/06/26:195.9700:196.2300:194.5000:194.8600:3320200:153.5145
IBM:2013/06/27:196.0500:196.9000:195.2900:195.6500:3514200:154.1368
IBM:2013/06/28:191.7300:192.6600:188.4100:191.1100:10055800:150.5602
IBM:2013/07/01:192.1500:193.8000:191.1200:191.2800:4442400:150.6941
Finance-QuoteHist-1.32/t/dat/split_plain.dat0000644000175000017500000000010014014003527017437 0ustar  sisksiskFinance::QuoteHist
NKE,2009/01/01,2013/10/01
NKE:2012/12/26:2:1
Finance-QuoteHist-1.32/t/dat/split_yahoo.dat0000644000175000017500000000010714014003521017454 0ustar  sisksiskFinance::QuoteHist::Yahoo
NKE,2009/01/01,2013/10/01
NKE:2012/12/26:2:1
Finance-QuoteHist-1.32/t/dat/quote_daily_plain.dat0000644000175000017500000004123313554074427020656 0ustar  sisksiskFinance::QuoteHist
IBM,2012/07/01,2013/07/01
IBM:2012/07/02:196.3600:197.2000:194.8500:195.8300:2827000:151.6017
IBM:2012/07/03:195.4600:196.3400:194.9100:195.9300:1450400:151.6791
IBM:2012/07/05:194.8800:196.8500:193.6300:195.2900:2690200:151.1836
IBM:2012/07/06:193.9200:193.9400:189.7400:191.4100:4952900:148.1799
IBM:2012/07/09:190.7600:191.0000:188.0500:189.6700:3988100:146.8329
IBM:2012/07/10:190.3000:191.1400:185.6000:186.2600:4690300:144.1931
IBM:2012/07/11:186.2200:187.3600:183.5100:185.2500:5456100:143.4112
IBM:2012/07/12:184.2500:184.3900:181.8500:183.0900:4931300:141.7390
IBM:2012/07/13:183.4600:186.3300:183.0300:186.0100:3933000:143.9995
IBM:2012/07/16:185.5800:186.1000:184.5800:184.7900:3144400:143.0551
IBM:2012/07/17:185.7300:186.2900:183.2000:183.6500:5158600:142.1725
IBM:2012/07/18:184.1500:188.5900:183.5500:188.2500:8019500:145.7336
IBM:2012/07/19:193.4000:196.8500:192.9700:195.3400:10395400:151.2224
IBM:2012/07/20:194.0900:194.9000:192.1700:192.4500:4789700:148.9851
IBM:2012/07/23:189.7800:191.3000:188.2000:190.8300:3904500:147.7309
IBM:2012/07/24:190.9200:191.3200:188.5600:190.3400:3597100:147.3516
IBM:2012/07/25:190.3100:192.7700:189.3200:191.0800:3833800:147.9244
IBM:2012/07/26:193.4900:194.9500:192.5700:193.9500:3282900:150.1463
IBM:2012/07/27:195.1000:197.4100:193.9500:196.3900:4177300:152.0352
IBM:2012/07/30:196.3200:197.8400:195.9200:196.6800:2787000:152.2597
IBM:2012/07/31:196.5000:197.5800:195.8800:195.9800:3507000:151.7178
IBM:2012/08/01:196.9600:197.8500:194.7200:195.1800:2559300:151.0985
IBM:2012/08/02:194.1600:196.6000:193.0200:194.4500:2812600:150.5334
IBM:2012/08/03:196.4800:198.9500:196.1600:198.5200:3278100:153.6842
IBM:2012/08/06:198.7600:199.9400:198.5200:198.7600:2337800:153.8699
IBM:2012/08/07:199.4300:200.8800:198.8000:199.9300:3209400:154.7757
IBM:2012/08/08:198.2700:199.6900:198.1600:199.0300:2068300:154.7368
IBM:2012/08/09:198.6200:199.4600:197.8900:198.4200:2160100:154.2626
IBM:2012/08/10:197.8700:199.3700:197.2400:199.2900:2600500:154.9389
IBM:2012/08/13:198.8800:199.9800:197.7900:199.0100:2418300:154.7213
IBM:2012/08/14:198.8800:199.3300:197.7200:198.2900:2562300:154.1615
IBM:2012/08/15:198.9000:199.3300:197.9200:198.4000:2111800:154.2470
IBM:2012/08/16:198.7700:201.3200:198.1200:200.8400:2729500:156.1441
IBM:2012/08/17:201.0800:202.0000:200.6600:201.2200:2551800:156.4395
IBM:2012/08/20:200.6900:201.1300:200.0300:200.5000:2336000:155.8796
IBM:2012/08/21:200.9900:201.0000:198.3700:198.6500:3111300:154.4414
IBM:2012/08/22:198.6400:198.6500:196.9000:197.2500:3296100:153.3530
IBM:2012/08/23:197.0400:197.3700:195.4400:195.7000:2757400:152.1479
IBM:2012/08/24:194.9600:198.1100:194.2000:197.7700:2639500:153.7573
IBM:2012/08/27:197.9600:198.3000:195.6100:195.6900:2498800:152.1401
IBM:2012/08/28:195.5600:196.1100:194.5000:194.8700:2539200:151.5026
IBM:2012/08/29:195.1300:196.0400:194.9000:195.0800:2141400:151.6659
IBM:2012/08/30:194.7700:195.4700:193.1800:193.3700:2740900:150.3364
IBM:2012/08/31:194.3100:195.9500:193.4600:194.8500:3193300:151.4870
IBM:2012/09/04:196.6100:197.1700:193.2500:194.5400:4514400:151.2460
IBM:2012/09/05:194.4100:195.8500:193.8900:195.0400:3312500:151.6348
IBM:2012/09/06:196.2600:199.4600:196.1100:199.1000:3931700:154.7913
IBM:2012/09/07:199.1200:199.5000:198.0800:199.5000:3413700:155.1022
IBM:2012/09/10:199.3900:201.8200:198.7300:200.9500:4208000:156.2295
IBM:2012/09/11:200.5500:203.4600:200.5100:203.2700:3910600:158.0332
IBM:2012/09/12:203.5200:204.6500:202.9600:203.7700:3284000:158.4219
IBM:2012/09/13:204.3800:206.8100:203.4600:206.3600:3879600:160.4356
IBM:2012/09/14:206.2400:207.6500:206.0500:206.8100:4057100:160.7854
IBM:2012/09/17:205.5500:207.9900:205.5500:207.1500:3275100:161.0498
IBM:2012/09/18:206.8800:207.8800:206.4800:207.0700:2476100:160.9875
IBM:2012/09/19:207.5500:207.5700:206.0100:206.4300:2605600:160.4900
IBM:2012/09/20:205.8500:206.9900:205.3000:206.1800:3739100:160.2956
IBM:2012/09/21:207.3600:207.9400:205.9200:205.9800:12771500:160.1401
IBM:2012/09/24:205.0200:206.0600:204.9000:205.2900:3511100:159.6037
IBM:2012/09/25:205.6000:207.3200:204.9500:204.9800:3345200:159.3627
IBM:2012/09/26:205.0500:205.9800:203.9000:204.0000:3423300:158.6008
IBM:2012/09/27:204.3700:206.9000:204.1400:205.9100:2771900:160.0857
IBM:2012/09/28:205.6600:208.3200:205.3100:207.4500:4910900:161.2830
IBM:2012/10/01:208.0100:211.7500:207.9400:210.4700:5456900:163.6309
IBM:2012/10/02:210.9600:211.4700:208.5000:209.8400:3321000:163.1411
IBM:2012/10/03:209.9400:211.3000:209.3000:210.5100:3192300:163.6620
IBM:2012/10/04:210.2800:210.9700:208.5300:210.3900:2957500:163.5687
IBM:2012/10/05:211.1500:211.7900:210.0600:210.5900:2858400:163.7242
IBM:2012/10/08:210.0200:210.6400:209.2100:209.8200:1982600:163.1255
IBM:2012/10/09:209.9500:210.7400:207.7000:207.9900:3580400:161.7029
IBM:2012/10/10:207.8200:208.2300:205.0500:205.8200:4024500:160.0157
IBM:2012/10/11:206.5700:207.2700:205.6300:205.7600:2910000:159.9691
IBM:2012/10/12:205.6400:207.9600:205.5600:207.8000:2712000:161.5551
IBM:2012/10/15:208.8800:209.4000:207.8200:208.9300:3060900:162.4336
IBM:2012/10/16:209.9400:211.0000:209.4700:211.0000:5632100:164.0430
IBM:2012/10/17:201.2100:203.4100:198.5500:200.6300:12673900:155.9807
IBM:2012/10/18:199.9100:200.1400:194.0700:194.9600:9295900:151.5726
IBM:2012/10/19:195.2900:196.0800:193.1800:193.3600:6620300:150.3287
IBM:2012/10/22:193.7100:194.6800:192.4400:194.4000:4402500:151.1372
IBM:2012/10/23:193.5300:194.0000:190.9400:191.2500:4427400:148.6882
IBM:2012/10/24:192.4400:193.2900:190.5600:190.7200:3738000:148.2762
IBM:2012/10/25:191.4100:192.4400:190.7100:191.6000:3249600:148.9603
IBM:2012/10/26:191.4900:193.8700:191.4700:193.2700:4006500:150.2587
IBM:2012/10/31:194.8000:196.4100:193.6300:194.5300:6052300:151.2383
IBM:2012/11/01:194.6800:197.8900:194.5500:197.1500:3758800:153.2752
IBM:2012/11/02:197.5300:198.0000:193.2900:193.4300:4260100:150.3831
IBM:2012/11/05:192.3600:194.9700:192.0100:194.1400:2736200:150.9351
IBM:2012/11/06:195.2600:196.7500:194.6800:195.0700:3281000:151.6581
IBM:2012/11/07:193.3600:193.6800:191.1600:191.1600:4746200:149.2687
IBM:2012/11/08:191.4000:191.8100:190.0900:190.1000:3542200:148.4410
IBM:2012/11/09:189.8500:191.6400:188.8800:189.6400:3671600:148.0817
IBM:2012/11/12:190.0300:190.7700:187.4100:189.2500:3067400:147.7772
IBM:2012/11/13:188.3900:190.8800:188.2500:188.3200:3406700:147.0510
IBM:2012/11/14:189.1400:189.2700:185.2800:185.5100:4404400:144.8568
IBM:2012/11/15:185.8300:187.2000:184.7800:185.8500:3411700:145.1223
IBM:2012/11/16:186.3300:187.5600:185.1000:186.9400:4660800:145.9735
IBM:2012/11/19:188.6200:190.3900:188.3000:190.3500:3588900:148.6362
IBM:2012/11/20:189.8800:190.1100:188.2500:189.2000:3170200:147.7382
IBM:2012/11/21:189.3000:190.8900:188.9500:190.2900:5189600:148.5893
IBM:2012/11/23:191.0000:193.4900:190.8000:193.4900:3877600:151.0881
IBM:2012/11/26:192.4500:193.3700:191.7700:192.8800:5210300:150.6118
IBM:2012/11/27:192.0800:193.0500:191.0000:191.2300:3290900:149.3233
IBM:2012/11/28:190.9800:192.0400:189.2700:191.9800:3603600:149.9090
IBM:2012/11/29:192.7500:192.9000:190.2000:191.5300:4077900:149.5576
IBM:2012/11/30:191.7500:192.0000:189.5000:190.0700:4936400:148.4175
IBM:2012/12/03:190.7600:191.3000:188.3600:189.4800:3349600:147.9569
IBM:2012/12/04:189.7100:190.7500:189.0200:189.3600:3990700:147.8632
IBM:2012/12/05:189.0100:189.4600:186.9400:188.6500:4202100:147.3087
IBM:2012/12/06:189.1700:190.1800:188.4100:189.7000:3216600:148.1286
IBM:2012/12/07:190.1400:192.2000:190.1100:191.9500:4091300:149.8856
IBM:2012/12/10:192.1700:193.5400:191.6500:192.6200:3438500:150.4087
IBM:2012/12/11:193.1800:194.8000:193.1500:194.2000:4144000:151.6425
IBM:2012/12/12:194.6000:194.7500:192.4600:192.9500:4241900:150.6664
IBM:2012/12/13:192.1800:193.0500:191.3700:191.9900:3623200:149.9168
IBM:2012/12/14:191.6900:193.4000:191.2600:191.7600:3826900:149.7372
IBM:2012/12/17:191.7600:193.9100:191.7600:193.6200:3792400:151.1896
IBM:2012/12/18:193.8500:195.9200:193.8400:195.6900:4125900:152.8060
IBM:2012/12/19:195.4600:196.4500:195.0000:195.0800:4270500:152.3296
IBM:2012/12/20:194.4700:194.9500:193.1600:194.7700:4184500:152.0876
IBM:2012/12/21:193.1000:194.5000:191.5700:193.4200:8225300:151.0334
IBM:2012/12/24:193.0500:193.8300:192.0200:192.4000:1631200:150.2370
IBM:2012/12/26:192.2100:193.5300:190.9500:191.9500:2776500:149.8856
IBM:2012/12/27:191.5300:193.2800:190.7000:192.7100:3967600:150.4790
IBM:2012/12/28:191.1100:192.2200:189.8300:189.8300:3536600:148.2301
IBM:2012/12/31:189.2300:191.6800:188.8400:191.5500:4684900:149.5732
IBM:2013/01/02:194.0900:196.3500:193.8000:196.3500:4234100:153.3213
IBM:2013/01/03:195.6700:196.2900:194.4400:195.2700:3644700:152.4780
IBM:2013/01/04:194.1900:194.4600:192.7800:193.9900:3380200:151.4785
IBM:2013/01/07:193.4000:193.7800:192.3400:193.1400:2862300:150.8148
IBM:2013/01/08:192.9200:193.3000:191.6000:192.8700:3026900:150.6040
IBM:2013/01/09:193.4800:193.4900:191.6500:192.3200:3212000:150.1745
IBM:2013/01/10:192.6500:192.9600:191.2800:192.8800:3608100:150.6118
IBM:2013/01/11:194.1500:195.0000:192.9000:194.4500:3880400:151.8377
IBM:2013/01/14:192.8200:193.2800:191.7500:192.6200:4172200:150.4087
IBM:2013/01/15:191.3100:192.7300:190.3900:192.5000:4172100:150.3150
IBM:2013/01/16:192.0000:193.1800:191.3500:192.5900:2962900:150.3853
IBM:2013/01/17:193.8500:194.4600:193.2400:193.6500:3881600:151.2130
IBM:2013/01/18:194.0300:195.0000:193.8000:194.4700:4559600:151.8533
IBM:2013/01/22:194.3600:196.0800:194.0100:196.0800:7172300:153.1105
IBM:2013/01/23:203.5000:208.5800:203.3600:204.7200:12530500:159.8571
IBM:2013/01/24:203.9100:205.0600:203.0800:204.4200:4447200:159.6228
IBM:2013/01/25:204.4500:205.1800:204.1300:204.9700:3358900:160.0523
IBM:2013/01/28:204.8500:206.2200:204.2900:204.9300:2823700:160.0211
IBM:2013/01/29:204.3400:205.7300:203.6400:203.9000:3617100:159.2167
IBM:2013/01/30:203.6900:204.8800:203.1900:203.5200:2998400:158.9201
IBM:2013/01/31:203.3200:204.4700:202.9600:203.0700:3091400:158.5687
IBM:2013/02/01:204.6500:205.3500:203.8400:205.1800:3370700:160.2163
IBM:2013/02/04:204.1900:205.0200:203.5700:203.7900:3188800:159.1309
IBM:2013/02/05:204.3100:204.7500:202.5100:202.7900:3636900:158.3501
IBM:2013/02/06:200.3900:201.2900:199.5600:201.0200:3624200:157.6286
IBM:2013/02/07:200.6200:200.9100:198.6800:199.7400:3076700:156.6249
IBM:2013/02/08:199.9700:202.0900:199.6800:201.6800:2893300:158.1461
IBM:2013/02/11:200.9800:201.9500:199.7500:200.1600:2944700:156.9543
IBM:2013/02/12:200.0100:200.7400:199.0200:200.0400:2461800:156.8602
IBM:2013/02/13:200.6500:200.9500:199.5700:200.0900:2169800:156.8994
IBM:2013/02/14:199.7300:200.3200:199.2600:199.6500:3294200:156.5544
IBM:2013/02/15:199.9800:201.2500:199.8200:200.9800:3627800:157.5973
IBM:2013/02/19:200.6000:201.8900:200.2200:200.3200:2998300:157.0797
IBM:2013/02/20:200.6200:201.7200:198.8600:199.3100:3715400:156.2877
IBM:2013/02/21:198.6300:199.0700:198.1100:198.3300:3922900:155.5193
IBM:2013/02/22:199.2300:201.0900:198.8400:201.0900:3107900:157.6835
IBM:2013/02/25:201.6700:202.4900:197.5100:197.5100:3844800:154.8763
IBM:2013/02/26:198.6300:199.9000:197.8400:199.1400:3391600:156.1544
IBM:2013/02/27:198.8900:202.7500:198.6000:202.3300:4185100:158.6559
IBM:2013/02/28:202.1800:203.1200:200.7900:200.8300:4689200:157.4796
IBM:2013/03/01:200.6500:202.9400:199.3600:202.9100:3308300:159.1106
IBM:2013/03/04:202.5900:205.1900:202.5500:205.1900:3693400:160.8985
IBM:2013/03/05:205.8600:207.7000:205.6900:206.5300:3807800:161.9493
IBM:2013/03/06:207.0300:208.4900:206.6600:208.3800:3594800:163.3999
IBM:2013/03/07:208.2900:209.6000:208.2400:209.4200:3884400:164.2154
IBM:2013/03/08:209.8500:210.7400:209.4300:210.3800:3700500:164.9682
IBM:2013/03/11:210.0400:210.2000:209.0400:210.0800:3049700:164.7330
IBM:2013/03/12:209.4000:210.7300:209.0900:210.5500:3591600:165.1015
IBM:2013/03/13:210.2000:212.3600:209.7700:212.0600:3355900:166.2856
IBM:2013/03/14:212.1500:215.8600:212.1500:215.8000:5505500:169.2183
IBM:2013/03/15:215.3800:215.9000:213.4100:214.9200:7930300:168.5283
IBM:2013/03/18:212.9000:214.5000:212.6400:213.2100:3006000:167.1874
IBM:2013/03/19:214.1300:215.1200:211.8300:213.4400:3198600:167.3677
IBM:2013/03/20:214.7600:215.8200:214.3000:215.0600:3019000:168.6380
IBM:2013/03/21:212.9600:213.0000:210.1100:212.2600:5830400:166.4424
IBM:2013/03/22:212.2100:213.1700:211.6200:212.0800:3031500:166.3013
IBM:2013/03/25:212.5400:212.8100:210.0500:210.7400:3242500:165.2505
IBM:2013/03/26:211.7700:212.5000:211.5000:212.3600:2300300:166.5208
IBM:2013/03/27:210.9600:212.1600:210.1000:210.8900:3223400:165.3681
IBM:2013/03/28:209.8300:213.4400:209.7400:213.3000:3749900:167.2579
IBM:2013/04/01:212.8000:213.5000:211.2500:212.3800:2144500:166.5365
IBM:2013/04/02:212.9200:214.7100:211.5700:214.3600:3116800:168.0891
IBM:2013/04/03:214.3200:214.8900:212.6300:212.6600:3128800:166.7561
IBM:2013/04/04:212.7700:212.9300:210.0400:211.3100:3709500:165.6975
IBM:2013/04/05:209.1000:209.8400:206.3400:209.4100:4148000:164.2076
IBM:2013/04/08:209.0700:209.3200:207.3300:209.3200:2351200:164.1370
IBM:2013/04/09:209.5600:210.0000:208.6100:209.2200:2577300:164.0587
IBM:2013/04/10:211.8600:212.5100:210.4700:212.0000:3524200:166.2385
IBM:2013/04/11:211.5000:213.0900:210.8500:212.9200:3655600:166.9599
IBM:2013/04/12:210.7900:211.4900:209.7800:211.3800:3165400:165.7524
IBM:2013/04/15:210.4400:211.6900:209.2600:209.2600:4218100:164.0900
IBM:2013/04/16:210.8400:212.0000:209.2000:212.0000:2859500:166.2385
IBM:2013/04/17:210.5300:211.0900:209.5000:209.6700:3269800:164.4115
IBM:2013/04/18:210.1100:210.2500:206.1500:207.1500:6502000:162.4354
IBM:2013/04/19:195.7400:196.5000:189.7600:190.0000:18847000:148.9874
IBM:2013/04/22:191.1500:191.3800:187.6800:187.8300:9845400:147.2858
IBM:2013/04/23:189.2700:192.5500:188.6100:191.6100:6296100:150.2498
IBM:2013/04/24:192.1600:192.5000:191.0900:191.7100:3693900:150.3283
IBM:2013/04/25:192.6900:195.1700:192.3200:193.9500:4650900:152.0847
IBM:2013/04/26:194.1600:194.7100:193.2500:194.3100:3490300:152.3671
IBM:2013/04/29:194.7800:199.6800:194.6500:199.1500:5932400:156.1623
IBM:2013/04/30:199.1300:202.5900:197.3500:202.5400:7539900:158.8206
IBM:2013/05/01:201.8700:202.1700:199.2000:199.6300:4899100:156.5387
IBM:2013/05/02:200.1200:202.4300:199.7500:202.3900:3945100:158.7029
IBM:2013/05/03:203.9400:205.3200:203.7500:204.5100:4492700:160.3653
IBM:2013/05/06:203.7900:203.9900:201.5200:202.7800:4826500:159.0087
IBM:2013/05/07:202.8100:203.7300:201.6500:203.6300:3451000:159.6753
IBM:2013/05/08:202.9400:204.8500:202.5100:204.8200:3601700:161.3612
IBM:2013/05/09:204.6900:205.0000:202.7200:203.2400:3542300:160.1164
IBM:2013/05/10:203.3700:204.5300:202.8200:204.4700:3279200:161.0855
IBM:2013/05/13:204.1800:204.4700:202.2200:202.4700:3648400:159.5098
IBM:2013/05/14:202.0900:203.6700:202.0800:203.2100:3699700:160.0928
IBM:2013/05/15:202.2500:203.6800:202.0400:203.3200:4028100:160.1795
IBM:2013/05/16:204.0000:206.5900:204.0000:204.6900:4507000:161.2587
IBM:2013/05/17:205.2500:209.5000:204.9900:208.4400:5704100:164.2131
IBM:2013/05/20:208.0200:209.1500:207.4200:207.6000:3614500:163.5513
IBM:2013/05/21:207.2400:209.4800:207.0000:208.6500:2982900:164.3785
IBM:2013/05/22:208.5000:210.1500:206.1200:206.9900:5186800:163.0707
IBM:2013/05/23:205.4800:208.6100:205.1300:206.1600:5179100:162.4169
IBM:2013/05/24:204.6900:206.4300:204.4200:205.7200:3295800:162.0702
IBM:2013/05/28:207.4400:208.5900:207.1600:207.7800:4011500:163.6931
IBM:2013/05/29:206.2200:208.4200:206.0400:207.9200:3439400:163.8034
IBM:2013/05/30:206.9700:210.9900:206.9700:209.3600:4399700:164.9379
IBM:2013/05/31:208.5900:211.9800:208.0200:208.0200:4949000:163.8822
IBM:2013/06/03:208.2500:210.0500:207.3300:208.9500:3322000:164.6149
IBM:2013/06/04:208.6000:209.0000:205.0800:206.1900:3598300:162.4405
IBM:2013/06/05:205.1600:206.2900:202.4000:202.7400:4215400:159.7225
IBM:2013/06/06:203.0000:204.2000:201.4700:203.8000:3679400:160.5576
IBM:2013/06/07:204.8500:206.3500:204.1100:206.3500:2953400:162.5666
IBM:2013/06/10:206.9700:206.9800:204.9000:205.0200:2652300:161.5188
IBM:2013/06/11:203.1700:205.9500:202.5100:203.9800:2814400:160.6994
IBM:2013/06/12:204.5900:205.2300:200.5100:201.2000:3409800:158.5093
IBM:2013/06/13:201.0100:204.4000:200.3400:203.7700:2899500:160.5339
IBM:2013/06/14:203.9700:204.7400:201.8100:202.2000:2804500:159.2971
IBM:2013/06/17:203.4400:205.1700:202.5500:203.0400:3219900:159.9588
IBM:2013/06/18:203.0200:206.0900:202.8700:204.8700:3277800:161.4006
IBM:2013/06/19:204.4400:205.0300:201.9300:201.9400:2846100:159.0923
IBM:2013/06/20:200.6700:201.7000:197.2800:197.3500:4514800:155.4762
IBM:2013/06/21:198.5000:198.5200:193.5400:195.4600:8914800:153.9872
IBM:2013/06/24:193.9900:194.9100:191.3400:193.5400:4356200:152.4746
IBM:2013/06/25:195.3100:195.5900:193.1500:194.9800:3846200:153.6090
IBM:2013/06/26:195.9700:196.2300:194.5000:194.8600:3320200:153.5145
IBM:2013/06/27:196.0500:196.9000:195.2900:195.6500:3514200:154.1368
IBM:2013/06/28:191.7300:192.6600:188.4100:191.1100:10055800:150.5602
IBM:2013/07/01:192.1500:193.8000:191.1200:191.2800:4442400:150.6941
Finance-QuoteHist-1.32/t/dat/dividend_yahoo.dat0000644000175000017500000000044313554074442020134 0ustar  sisksiskFinance::QuoteHist::Yahoo
INTC,2011/01/01,2013/10/01
INTC:2011/02/03:0.181
INTC:2011/05/04:0.181
INTC:2011/08/03:0.21
INTC:2011/11/03:0.21
INTC:2012/02/03:0.21
INTC:2012/05/03:0.21
INTC:2012/08/03:0.225
INTC:2012/11/05:0.225
INTC:2013/02/05:0.225
INTC:2013/05/03:0.225
INTC:2013/08/05:0.225
Finance-QuoteHist-1.32/t/dat/quote_weekly_yahoo.dat0000644000175000017500000000730213554074436021067 0ustar  sisksiskFinance::QuoteHist::Yahoo
IBM,2012/07/01,2013/07/01
IBM:2012/06/25:192.4800:195.8100:188.9000:195.5800:18637100:151.4082
IBM:2012/07/02:196.3600:197.2000:189.7400:191.4100:11920500:148.1799
IBM:2012/07/09:190.7600:191.1400:181.8500:186.0100:22998800:143.9995
IBM:2012/07/16:185.5800:196.8500:183.2000:192.4500:31507600:148.9851
IBM:2012/07/23:189.7800:197.4100:188.2000:196.3900:18795600:152.0352
IBM:2012/07/30:196.3200:198.9500:193.0200:198.5200:14944000:153.6842
IBM:2012/08/06:198.7600:200.8800:197.2400:199.2900:12376100:154.2802
IBM:2012/08/13:198.8800:202.0000:197.7200:201.2200:12373700:156.4395
IBM:2012/08/20:200.6900:201.1300:194.2000:197.7700:14140300:153.7573
IBM:2012/08/27:197.9600:198.3000:193.1800:194.8500:13113600:151.4870
IBM:2012/09/03:196.6100:199.5000:193.2500:199.5000:15172300:155.1022
IBM:2012/09/10:199.3900:207.6500:198.7300:206.8100:19339300:160.7854
IBM:2012/09/17:205.5500:207.9900:205.3000:205.9800:24867400:160.1401
IBM:2012/09/24:205.0200:208.3200:203.9000:207.4500:17962400:161.2830
IBM:2012/10/01:208.0100:211.7900:207.9400:210.5900:17786100:163.7242
IBM:2012/10/08:210.0200:210.7400:205.0500:207.8000:15209500:161.5551
IBM:2012/10/15:208.8800:211.0000:193.1800:193.3600:37283100:150.3287
IBM:2012/10/22:193.7100:194.6800:190.5600:193.2700:19824000:150.2587
IBM:2012/10/29:194.8000:198.0000:193.2900:193.4300:14071200:150.3831
IBM:2012/11/05:192.3600:196.7500:188.8800:189.6400:17977200:147.4365
IBM:2012/11/12:190.0300:190.8800:184.7800:186.9400:18951000:145.9735
IBM:2012/11/19:188.6200:193.4900:188.2500:193.4900:15826300:151.0881
IBM:2012/11/26:192.4500:193.3700:189.2700:190.0700:21119100:148.4175
IBM:2012/12/03:190.7600:192.2000:186.9400:191.9500:18850300:149.8856
IBM:2012/12/10:192.1700:194.8000:191.2600:191.7600:19274500:149.7372
IBM:2012/12/17:191.7600:196.4500:191.5700:193.4200:24598600:151.0334
IBM:2012/12/24:193.0500:193.8300:189.8300:189.8300:11911900:148.2301
IBM:2012/12/31:189.2300:196.3500:188.8400:193.9900:15943900:151.4785
IBM:2013/01/07:193.4000:195.0000:191.2800:194.4500:16589700:151.8377
IBM:2013/01/14:192.8200:195.0000:190.3900:194.4700:19748400:151.8533
IBM:2013/01/21:194.3600:208.5800:194.0100:204.9700:27508900:160.0523
IBM:2013/01/28:204.8500:206.2200:202.9600:205.1800:15901300:160.2163
IBM:2013/02/04:204.1900:205.0200:198.6800:201.6800:16419900:157.4833
IBM:2013/02/11:200.9800:201.9500:199.0200:200.9800:14498300:157.5973
IBM:2013/02/18:200.6000:201.8900:198.1100:201.0900:13744500:157.6835
IBM:2013/02/25:201.6700:203.1200:197.5100:202.9100:19419000:159.1106
IBM:2013/03/04:202.5900:210.7400:202.5500:210.3800:18680900:164.9682
IBM:2013/03/11:210.0400:215.9000:209.0400:214.9200:23433000:168.5283
IBM:2013/03/18:212.9000:215.8200:210.1100:212.0800:18085500:166.3013
IBM:2013/03/25:212.5400:213.4400:209.7400:213.3000:12516100:167.2579
IBM:2013/04/01:212.8000:214.8900:206.3400:209.4100:16247600:164.2076
IBM:2013/04/08:209.0700:213.0900:207.3300:211.3800:15273700:165.7524
IBM:2013/04/15:210.4400:212.0000:189.7600:190.0000:35696400:148.9874
IBM:2013/04/22:191.1500:195.1700:187.6800:194.3100:27976600:152.3671
IBM:2013/04/29:194.7800:205.3200:194.6500:204.5100:26809200:160.3653
IBM:2013/05/06:203.7900:205.0000:201.5200:204.4700:18700700:160.3339
IBM:2013/05/13:204.1800:209.5000:202.0400:208.4400:21587300:164.2131
IBM:2013/05/20:208.0200:210.1500:204.4200:205.7200:20259100:162.0702
IBM:2013/05/27:207.4400:211.9800:206.0400:208.0200:16799600:163.8822
IBM:2013/06/03:208.2500:210.0500:201.4700:206.3500:17768500:162.5666
IBM:2013/06/10:206.9700:206.9800:200.3400:202.2000:14580500:159.2971
IBM:2013/06/17:203.4400:206.0900:193.5400:195.4600:22773400:153.9872
IBM:2013/06/24:193.9900:196.9000:188.4100:191.1100:25092600:150.5602
IBM:2013/07/01:192.1500:195.1600:190.2600:194.9300:12378100:153.5696
Finance-QuoteHist-1.32/t/dat/csv.dat0000644000175000017500000000326612453534725015752 0ustar  sisksiskDate,Open,High,Low,Close,Volume,Adj. Close*
20-Jul-05,83.20,84.96,82.99,84.60,9323200,84.60
19-Jul-05,84.72,85.11,83.70,83.70,14149700,83.70
18-Jul-05,81.99,83.94,81.68,81.81,8705600,81.81
15-Jul-05,82.43,82.75,81.52,82.38,7644100,82.38
14-Jul-05,82.00,82.67,82.00,82.42,8546200,82.42
13-Jul-05,81.16,81.75,81.16,81.45,11340000,81.45
12-Jul-05,79.20,80.49,79.18,80.04,10945500,80.04
11-Jul-05,79.40,79.52,78.52,78.96,7186700,78.96
8-Jul-05,77.38,79.52,77.14,79.30,13440500,79.30
7-Jul-05,75.15,77.53,75.00,77.38,10757200,77.38
6-Jul-05,74.80,76.15,74.40,75.81,8009300,75.81
5-Jul-05,74.38,74.97,74.16,74.79,5181800,74.79
1-Jul-05,74.30,75.33,74.30,74.67,4353100,74.67
30-Jun-05,74.80,75.49,74.07,74.20,6127000,74.20
29-Jun-05,75.26,75.68,74.62,74.73,5519100,74.73
28-Jun-05,73.83,75.30,73.45,75.30,8188800,75.30
27-Jun-05,74.01,74.77,73.50,73.88,6489800,73.88
24-Jun-05,75.17,75.40,74.00,74.01,10792100,74.01
23-Jun-05,76.81,76.97,75.06,75.41,7766700,75.41
22-Jun-05,76.83,77.49,76.67,77.23,5646300,77.23
21-Jun-05,76.70,77.00,76.11,76.41,5339100,76.41
20-Jun-05,76.03,76.98,75.57,76.55,3745600,76.55
17-Jun-05,77.70,77.73,76.38,76.39,8593800,76.39
16-Jun-05,76.40,77.25,76.31,77.05,7840000,77.05
15-Jun-05,75.70,76.50,75.15,76.30,7103600,76.30
14-Jun-05,75.05,75.43,74.73,74.89,4314900,74.89
13-Jun-05,74.50,75.93,74.45,75.05,5715700,75.05
10-Jun-05,74.75,75.05,74.10,74.77,4895800,74.77
9-Jun-05,74.58,75.47,74.23,74.93,4423200,74.93
8-Jun-05,75.04,75.40,74.63,74.80,4280000,74.80
7-Jun-05,75.00,76.09,75.00,75.04,5226600,75.04
6-Jun-05,75.80,75.90,74.92,75.00,5978600,75.00
3-Jun-05,77.06,77.10,75.74,75.79,6149900,75.79
2-Jun-05,76.75,77.39,76.68,77.35,4025600,77.35
1-Jun-05,75.57,77.50,75.57,76.84,7380600,76.84
Finance-QuoteHist-1.32/t/20_dividends.t0000644000175000017500000000176712453534725016360 0ustar  sisksiskuse strict;

my $tcount;
BEGIN { $tcount = 4 }
use Test::More tests => $tcount;

use FindBin;
use lib $FindBin::RealBin;

use testload;

SKIP: {
  skip("dividend (no connect)", $tcount) unless network_ok();
  for my $src (sources()) {
    SKIP: {
      my($m, $sym, $start, $end, $dat) = basis($src, 'dividend');
      next unless $m;
      skip("(dev only) dividend $src test", 2)
        unless DEV_TESTS || $src eq GOLDEN_CHILD;
      eval "use $m";
      my %parms = ( class => $m );
      dividend_cmp(
        $sym, $start, $end,
        "direct dividend ($src)",
        $dat, %parms
      );
    }
  }
}

sub dividend_cmp {
  @_ >= 5 or die "Problem with args\n";
  my($symbol, $start_date, $end_date, $label, $dat, %parms) = @_;
  my $q = new_quotehist($symbol, $start_date, $end_date, %parms);
  my @rows = $q->dividends;
  cmp_ok(scalar @rows, '==', scalar @$dat, "$label (rows)");
  for my $i (0 .. $#rows) {
    $rows[$i] = join(':', @{$rows[$i]});
  }
  is_deeply(\@rows, $dat, "$label (content)");
}
Finance-QuoteHist-1.32/t/testload.pm0000644000175000017500000000727713123017016016060 0ustar  sisksiskpackage testload;

use strict;
use Test::More;
use File::Spec;
use LWP::UserAgent;
use HTTP::Request;

use Finance::QuoteHist;

use constant DEV_TESTS => $ENV{FQH_DEV_TESTS};

use constant GOLDEN_CHILD => 'yahoo';

use vars qw( @ISA @EXPORT );

require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(
  network_ok
  new_quotehist
  modules
  all_modules
  sources
  modes
  granularities
  basis
  csv_content
  GOLDEN_CHILD
  DEV_TESTS
);

my($Dat_Dir, $Mod_Dir);
BEGIN {
  my($vol, $dir, $file) = File::Spec->splitpath(__FILE__);
  my @parts = File::Spec->splitdir($dir);
  pop @parts while @parts && $parts[-1] ne 't';
  my $ddir = File::Spec->catdir(@parts, 'dat');
  $Dat_Dir = File::Spec->catpath($vol, $ddir, '');
  pop @parts;
  my $mdir = File::Spec->catdir(@parts, 'lib', 'Finance', 'QuoteHist');
  $Mod_Dir = File::Spec->catpath($vol, $mdir, '');
}

my $csv_txt;
my $csv_file = "$Dat_Dir/csv.dat";
open(F, '<', $csv_file) or die "problem reading $csv_file : $!";
$csv_txt = join('', );
close(F);

sub csv_content { $csv_txt }

my(%Modules, %Files);

for my $f (glob("$Dat_Dir/*.dat")) {
  my($vol, $dir, $label) = File::Spec->splitpath($f);
  $label =~ s/\.dat$//;
  next unless $label =~ /^(quote|dividend|split)_/;
  open(F, '<', $f) or die "problem reading $f : $!";
  my @lines = ;
  chomp @lines;
  close(F);
  my $class = shift @lines;
  ++$Modules{$class};
  my($sym, $start, $end) = split(/,/, shift @lines);
  if ($1 eq 'quote') {
    my($mode, $gran, $source) = split(/_/, $label);
    if ($lines[0] =~ tr/:/:/ > 5) {
      # drop adjusted and volume, they've proven to be too
      # variable for testing
      for my $i (0 .. $#lines) {
        my @line = split(/:/, $lines[$i]);
        pop @line while @line > 6;
        $lines[$i] = join(':', @line);
      }
    }
    $Files{$source}{$mode}{$gran} = [$class, $sym, $start, $end, \@lines];
  }
  else {
    my($mode, $source) = split(/_/, $label);
    $Files{$source}{$mode} = [$class, $sym, $start, $end, \@lines];
  }
}

my $Network_Up;

sub network_ok {
  if (! defined $Network_Up) {
    my %ua_parms;
    if ($ENV{http_proxy}) {
      $ua_parms{env_proxy} = 1;
    }
    my $ua = LWP::UserAgent->new(%ua_parms)
      or die "Problem creating user agent\n";
    #my $request = HTTP::Request->new('HEAD', 'https://finance.yahoo.com')
    my $request = HTTP::Request->new('HEAD', 'https://www.google.com')
      or die "Problem creating http request object\n";
    my $response = $ua->request($request, @_);
    $Network_Up = $response->is_redirect || $response->is_success;
    if (!$Network_Up) {
      print STDERR "Problem with net fetch: ", $response->status_line, "\n";
    }
  }
  $Network_Up;
}

sub new_quotehist {
  my($symbols, $start_date, $end_date, %parms) = @_;
  my $class = $parms{class} || 'Finance::QuoteHist';
  delete $parms{class};
  $class->new(
    symbols    => $symbols,
    start_date => $start_date,
    end_date   => $end_date,
    auto_proxy => 1,
    %parms,
  );
}

sub modules { sort keys %Modules }

sub sources { sort keys %Files }

sub modes {
  my $src = shift || return;
  my $h = $Files{$src} || return;
  sort keys %$h;
}

sub granularities {
  my $src = shift || return;
  my $h = $Files{$src}{quote} || return;
  sort keys %$h;
}

sub basis {
  my($src, $mode, $gran) = @_;
  my $basis;
  if ($mode eq 'quote') {
    $basis = $Files{$src}{$mode}{$gran};
  }
  else {
    $basis = $Files{$src}{$mode};
  }
  return unless $basis;
  @$basis;
}

sub all_modules {
  my %mods;
  for my $f (glob "$Mod_Dir/*.pm") {
    my($vol, $dir, $base) = File::Spec->splitpath($f);
    $base =~ s/\.pm$//;
    next if $base eq 'Generic';
    $mods{lc($base)} = "Finance::QuoteHist::$base";
  }
  $mods{plain} = "Finance::QuoteHist";
  wantarray ? %mods : \%mods;
}

1;
Finance-QuoteHist-1.32/t/30_splits.t0000644000175000017500000000174114014003564015701 0ustar  sisksiskuse strict;

my $tcount;
BEGIN { $tcount = 4 }
use Test::More tests => $tcount;

use FindBin;
use lib $FindBin::RealBin;
use testload;

SKIP: {
  skip("split (no connect)", $tcount) unless network_ok();
  for my $src (sources()) {
    SKIP: {
      my($m, $sym, $start, $end, $dat) = basis($src, 'split');
      next unless $m;
      skip("(dev only) split $src test", 2)
        unless DEV_TESTS || $src eq GOLDEN_CHILD;
      eval "use $m";
      my %parms = ( class => $m );
      split_cmp(
        $sym, $start, $end,
        "direct split ($src)",
        $dat, %parms
      );
    }
  }
}

sub split_cmp {
  @_ >= 5 or die "Problem with args\n";
  my($symbol, $start_date, $end_date, $label, $dat, %parms) = @_;
  my $q = new_quotehist($symbol, $start_date, $end_date, %parms);
  my @rows = $q->splits;
  cmp_ok(scalar @rows, '==', scalar @$dat, "$label (rows)");
  for my $i (0 .. $#rows) {
    $rows[$i] = join(':', @{$rows[$i]});
  }
  is_deeply(\@rows, $dat, "$label (content)");
}
Finance-QuoteHist-1.32/t/00_basic.t0000644000175000017500000000043112453775511015451 0ustar  sisksiskuse FindBin;
use lib $FindBin::RealBin;
use testload;

my $tcount;
BEGIN {
  my @mods = modules();
  $tcount = scalar @mods + 2;
}

use Test::More tests => $tcount;

BEGIN {
  use_ok('Finance::QuoteHist');
  use_ok('Finance::QuoteHist::Generic');
  use_ok($_) foreach modules();
}
Finance-QuoteHist-1.32/META.yml0000664000175000017500000000117414021707227014711 0ustar  sisksisk---
abstract: unknown
author:
  - unknown
build_requires:
  ExtUtils::MakeMaker: '0'
configure_requires:
  ExtUtils::MakeMaker: '0'
dynamic_config: 1
generated_by: 'ExtUtils::MakeMaker version 7.34, CPAN::Meta::Converter version 2.150010'
license: unknown
meta-spec:
  url: http://module-build.sourceforge.net/META-spec-v1.4.html
  version: '1.4'
name: Finance-QuoteHist
no_index:
  directory:
    - t
    - inc
requires:
  Date::Manip: '0'
  HTML::TableExtract: '2.07'
  HTTP::Request: '0'
  JSON: '0'
  LWP::UserAgent: '0'
  MIME::Base64: '0'
  Text::CSV: '0'
version: '1.32'
x_serialization_backend: 'CPAN::Meta::YAML version 0.018'