package Finance::FuturesQuote;

use vars qw/$VERSION %categories/;

$VERSION = 0.01;

=head1 NAME

Finance::FuturesQuote - Get major futures price board quotes from INO.com

=head1 SYNOPSIS

	use strict;
        use Finance::FuturesQuote;
        
        my %quotes = Finance::FuturesQuote::GetFuturesQuotes(qw[CL BO]);
        my %quotes = Finance::FuturesQuote::GetAllFuturesQuotes();

=head1 DESCRIPTION

This module gets quotes from INO.com for all major futures price board.
The B<GetFuturesQuotes> function will return quotes for the specified symbols, 
while the B<GetAllFuturesQuotes> function will return a quote for each of the 
known symbols. The download operation is efficient: only one request
is made even if several symbols are requested at once. 

=cut

use strict;
use Carp;
use warnings;
use LWP::UserAgent;
use HTTP::Request;
use HTML::TableContentParser;
# use Data::Dumper;

=head2 AVAILABLE SYMBOLS

Finance::FuturesQuote can only access quotes for futures in the following
price board categories
    
    Energy
    Metals
    Food and Fiber
    Grains and Oilseeds
    Interest Rates
    Livestock and Meats
    
Each category is an entry in a global hash, with the value being a hash of 
all available symbols and their respective names. To see a complete listing 
of all known symbols and names from the program, 

    foreach my $category (keys %Finance::FuturesQuote::categories) {
        print "$category =>\n";
        foreach (keys %{$Finance::FuturesQuote::categories{$category}}) {
            print "    $_ => $Finance::FuturesQuote::categories{$category}{$_}\n";
        }
    }

=cut

our %categories = (
		'Energy' => {
			   'HU' => 'New York Harbor Unleaded Gasoline',
			   'HO' => 'Heating Oil',
			   'NG' => 'Henry Hub Natural Gas',
			   'CL' => 'Light Sweet Crude Oil',
			   'PN' => 'Propane'
                	    },
		'Metals' => {
			   'GC' => 'Gold',
			   'AF' => 'Aluminum',
			   'YG' => 'Mini NY Gold',
			   'YI' => 'Mini NY Silver',
			   'PA' => 'Palladium',
			   'SI' => 'Silver',
			   'PL' => 'Platinum',
			   'HG' => 'Copper'
			    },
		'Food and Fiber' => {
			   'OJ' => 'Orange Juice Froz. Conc.',
			   'DA' => 'BFP Milk',
  			   'SB' => 'Sugar',
  			   'DB' => 'Butter',
  			   'KC' => 'Coffee',
  			   'LB' => 'Random Length Lumber',
  			   'SE' => 'Sugar',
  			   'CT' => 'Cotton',
  			   'CC' => 'Cocoa',
  			   'NF' => 'Nonfat Dry Milk',
  			   'OD' => 'Orange Juice Froz. Conc. Diff.',
				    },
		'Grains and Oilseeds' => {
			   'VW' => 'Wheat',
 			   'WU' => 'Wheat',
 			   'ZO' => 'Oats',
 			   'ZM' => 'Soybean Meal',
 			   'SX' => 'Soybeans',
 			   'VZ' => 'Corn',
 			   'BO' => 'Soybean Oil',
  			   'SM' => 'Soybean Meal',
  			   'YW' => 'Mini Wheat',
  			   'YK' => 'Mini Soybeans',
  			   'RR' => 'Rough Rice',
  			   'YC' => 'Mini Corn',
  			   'VL' => 'Soybean Oil',
  			   'VQ' => 'Rough Rice',
  			   'CU' => 'Corn',
  			   'OZ' => 'Oats',
  			   'VS' => 'Soybeans',
					 },
		'Interest Rates' => {
			   'ZB' => 'U.S. Treasury Bond',
  			   'TY' => 'Treasury Notes 10yr',
  			   'US' => 'U.S. Treasury Bond',
  			   'TU' => 'Treasury Notes 2yr',
  			   'EM' => 'Libor 1month',
  			   'EL' => 'Euroyen',
  			   'ZQ' => 'Fed Funds 30d',
  			   'ZN' => 'Treasury Notes 10yr',
  			   'FF' => 'Federal Funds 30day',
  			   'TE' => '28-Day Mexican TIIE',
  			   'ZT' => 'Treasury Notes 2yr',
  			   'ZF' => 'Treasury Notes 5yr',
  			   'EY' => 'Euroyen (TIBOR)',
  			   'FV' => 'Treasury Notes 5yr',
  			   'ED' => 'Eurodollar',
  			   'AN' => 'Agency Notes 10yr',
				    },
		'Livestock and Meats' => {
  			   'LH' => 'Lean Hogs',
    			   'LC' => 'Live Cattle',
    			   'PB' => 'Frozen Pork Bellies',
    			   'FC' => 'Feeder Cattle',
					 }
		  );

##### Public Functions #####
=head2 METHODS

=over 4

=item GetFuturesQuotes(list)

This function takes a list of symbols as the only arguement. The user can pass 
in as few or as many symbols as desired. The module will make a single access 
of the data to improve efficiency. If the list starts getting too long, it might 
just be easier to use the second method instead. The return format is outlined 
below.

=cut

sub GetAllFuturesQuotes {
  my @symbols;
  
  foreach my $group (keys %categories) {
    foreach (keys %{$categories{$group}}) {
      push @symbols,$_;
    }
  }
  return GetFuturesQuotes(@symbols);
}

=item GetAllFuturesQuotes()

This function will get the quotes for all known symbols. The return format is 
outlined below.

=back 4

=cut

sub GetFuturesQuotes {
  my %quotes;
  
  my $rawdata = _retrieveQuotes();
  foreach (@_) {
    _findQuote(\$rawdata, $_, \%quotes);
  }
  return %quotes;  
}

##### Internal Functions #####

sub _retrieveQuotes {
  my $ua = LWP::UserAgent->new;	                                             # Create a new UserAgent
  $ua->agent('Mozilla/25.'.(localtime)." (PERL ".__PACKAGE__." $VERSION");   # Give it a type name
  my $url = 'http://quotes.ino.com/exchanges/futboard/';
  my $req = new HTTP::Request('GET',$url) or die "Could not GET.\n" and return undef;
  my $res = $ua->request($req);                                              # $res is the object UA returned
  if (not $res->is_success()) {                                              # If successful
    warn "Failed to GET.\n";
    return undef;
  }
  return $res->content;
}

sub _findQuote {
  my $dataref = shift;
  my $symbol = shift;
  my $href = shift;

  my $p = HTML::TableContentParser->new();
  my $table = $p->parse($$dataref);
  my ($tblnum,$rownum) = _getGroupDataStart($table, _getGroupName($symbol));
  die "Cannot find group heading for $symbol." unless defined $tblnum and defined $rownum;
  
  for $rownum ($rownum+2 .. scalar(@{$$table[$tblnum]{rows}})-1) {
    if ($$table[$tblnum]{rows}[$rownum]{cells}[0]{data} =~ />$symbol/) {
      # print "Found $symbol\n" . Dumper($$table[$tblnum]{rows}[$rownum]{cells});
      $$href{$symbol}{Last}   = $$table[$tblnum]{rows}[$rownum]{cells}[1]{data};
      $$href{$symbol}{Change} = $$table[$tblnum]{rows}[$rownum]{cells}[2]{data};
      $$href{$symbol}{Volume} = $$table[$tblnum]{rows}[$rownum]{cells}[3]{data};
      $$href{$symbol}{OI}     = $$table[$tblnum]{rows}[$rownum]{cells}[4]{data};
      $$href{$symbol}{Time}   = $$table[$tblnum]{rows}[$rownum]{cells}[5]{data};
      
      $$href{$symbol}{Change} =~ />(.*?)</;
      $$href{$symbol}{Change} = $1;
      return;
    }
  }
}

sub _getGroupName {
  my $n = shift;
  
  foreach my $group (keys %categories) {
    foreach (keys %{$categories{$group}}) {
      return $group if ($ _ eq $n);
    }
  }
  return undef;
}

sub _getGroupDataStart {
  my $table = shift;
  my $group = shift;
  
  die "Undefined group heading." unless defined $group;
  
  # find the start of the table data for this symbol's group
  for my $tblnum (0 .. scalar(@{$table})-1) {
    for my $rownum (0 .. scalar(@{$$table[$tblnum]{rows}})-1) {
      next unless exists $$table[$tblnum]{rows}[$rownum]{cells}[0]{data};
      if ($$table[$tblnum]{rows}[$rownum]{cells}[0]{data} =~ /<a name="$group"/i) {
      	# print "Found start of $groupname at $tblnum,$rownum\n" . Dumper($$table[$tblnum]{rows}[$rownum]);
      	return ($tblnum,$rownum);
      }
    }
  }
  return (undef,undef);
}

=head2 RETURN FORMAT

The return value of either function is an hash of hashes, with the following style structure:
   
   $VAR1 = {
            'CL' => {
                     'Change' => '0.00',
                     'Last' => '60.73',
                     'Time' => 'set 14:46',
                     'Volume' => '1643',
                     'OI' => '200914'
                    },
            'SX' => {
                     'Change' => '0',
                     'Last' => '707 1/2',
                     'Time' => 'set 14:24',
                     'Volume' => '52238',
                     'OI' => '185920'
                    }
            };

where the keys of the hash are the requested symbols and each value is a hash
of all information available from  http://quotes.ino.com/exchanges/futboard/

=head1 PROBLEMS?

If this doesn't work, INO has probably changed their HTML format.
Let me know and I'll fix the code. Or by all means send a patch.

=head1 SEE ALSO

L<LWP::UserAgent>, L<HTTP::Request>, L<HTML::TableContentParser>.

=head1 AUTHOR

Paul Grinberg, gri6507 -at- yahoo -dot- com.

=cut

1;