# # Finance::QuoteOptions Module # Extract options prices and series information from the web. # # (C) Copyright 2007-2009 Kirk Bocek # Version 0.20 Contributions by Dan Dascalescu # package Finance::QuoteOptions; #require 5.6.1; use 5.006001; use strict; use WWW::Mechanize; #use LWP::UserAgent; #See Changes document use HTML::TokeParser; # set the version for version checking our $VERSION = 0.20; ############################ # Start of class definitions ############################ sub new { my $class = shift; my $self = {}; $self->{source} = 'yahoo'; $self->{symbol} = undef; $self->{proxy} = undef; $self->{symbol} = shift if @_; #Set symbol if provided $self->{symbol} = uc $self->{symbol} if $self->{symbol}; bless ($self, $class); return $self; } sub symbol { #Set or return target symbol my $self = shift; if (not @_) { return $self->{symbol}; } $self->{symbol} = shift; $self->{data} = []; $self->{success} = undef; $self->{status} = undef; $self->{response} = undef; } sub source { #Set or return data source #Only 'yahoo' or 'cboe' is accepted #Set source to 'yahoo' if anything else is provided my $self = shift; return $self->{source} unless @_; my $param = shift; $self->{source} = 'yahoo'; $self->{source} = 'cboe' if lc($param) eq 'cboe'; return $self->{source}; } sub retrieve { #get data my ($self, $expirations) = @_; return 0 unless $self->{symbol}; if ($self->{source} eq 'cboe') { $self->getcboedata(); } else { #Yahoo is the default $self->getyahoodata($expirations); } return $self->{success}; } sub expirations { #Return arrayref of all expiration dates my $self = shift; my $dates = []; push @$dates, $_->{exp} foreach @{$self->{data}}; return $dates; } sub calls { #Return arrayref with all calls for a given expiration #If param is 6 or 8 characters then it's an expiration date #3 or fewer characters and it's number of expirations out #Date can be ###, YYYYMM or YYYYMMDD my $self = shift; my $exp = shift; return if not defined $exp or $exp < 0; #Check if too many expirations out: return if length($exp) < 4 and $exp > $#{$self->{data}}; #If not number of exp out, then param must be 6 or 8 chars long return if length($exp) > 3 and length($exp) !~ /^[68]$/; $exp += 0; #Make sure it's numeric return $self->{data}->[$exp]->{calls} if length $exp < 4; #Param is date foreach (@{$self->{data}}) { return $_->{calls} if length $exp == 6 and $exp == substr($_->{exp},0,6); return $_->{calls} if length $exp == 8 and $exp == $_->{exp}; } return; } sub puts { #Return all puts for a given expiration #See calls() above my $self = shift; my $exp = shift; return if not defined $exp or $exp < 0; return if length($exp) < 4 and $exp > $#{$self->{data}}; return if length($exp) > 3 and length($exp) !~ /^[68]$/; $exp += 0; #Make sure it's numeric return $self->{data}->[$exp]->{puts} if length $exp < 4; foreach (@{$self->{data}}) { return $_->{puts} if length $exp == 6 and $exp == substr($_->{exp},0,6); return $_->{puts} if length $exp == 8 and $exp == $_->{exp}; } return; } sub callsymbols { #Return arrayref with all call symbols for a given expiration my $self = shift; my $exp = shift; return if $exp < 0; return unless defined $exp and $exp <= $#{$self->data}; $exp+=0; my $ret = []; push @$ret, $_->{symbol} foreach @{$self->{data}->[$exp]->{calls}}; return $ret; } sub putsymbols { #Return arrayref with all put symbols for a given expiration my $self = shift; my $exp = shift; return if $exp < 0; return unless defined $exp and $exp <= $#{$self->data}; $exp+=0; my $ret = []; push @$ret, $_->{symbol} foreach @{$self->{data}->[$exp]->{puts}}; return $ret; } sub callstrikes { #Return arrayref with all call strike prices for a given expiration my $self = shift; my $exp = shift; return if $exp < 0; return unless defined $exp and $exp <= $#{$self->data}; $exp+=0; my $ret = []; push @$ret, $_->{strike} foreach @{$self->{data}->[$exp]->{calls}}; return $ret; } sub putstrikes { #Return arrayref with all put strike prices for a given expiration my $self = shift; my $exp = shift; return if $exp < 0; return unless defined $exp and $exp <= $#{$self->data}; $exp+=0; my $ret = []; push @$ret, $_->{strike} foreach @{$self->{data}->[$exp]->{puts}}; return $ret; } #------------------------------------------------------------------- sub option { #Retrieve a single option my $self = shift; my $sym = shift; return unless $sym; my $ret = undef; my $date = undef; my $opt = undef; MAIN: for my $exp (@{$self->{data}}) { $date = $exp->{exp}; for my $o (@{$exp->{calls}}) { if (lc $o->{symbol} eq lc $sym) { $opt = $o; last MAIN; } } for my $o (@{$exp->{puts}}) { if (lc $o->{symbol} eq lc $sym) { $opt = $o; last MAIN; } } } #Copy the found option to a new annonymous hash #Since we need to add the {exp} key if ($opt) { $ret = {}; %$ret = %$opt; $ret->{exp} = $date; } return $ret; } sub success { #Set or retrieve success my $self = shift; my $stat = shift; if (defined $stat) { $self->{success} = $stat; } return $self->{success}; } sub status { #Set or retrieve status my $self = shift; my $stat = shift; $self->{status} = $stat if defined $stat; return $self->{status}; } sub response { #Set or retrieve response my $self = shift; my $stat = shift; $self->{response} = $stat if defined $stat; return $self->{response}; } sub proxy { #Set or retrieve proxy setting my $self = shift; my $stat = shift; $self->{proxy} = $stat if defined $stat; return $self->{proxy}; } sub data { #Return reference to data hash my $self = shift; return $self->{data}; } sub version { #Return version number my $self = shift; return $VERSION; } #------------------------------------------------------------------- sub getyahoodata { # # Main query page: # http://finance.yahoo.com/q/op?s=DIA # Additional expirations: # http://finance.yahoo.com/q/op?s=DIA&m=2007-06 # # The main query page yields options for only the next expiration. # At the top of those tables is a list of other expiration months. # Generate the URLs for those additional pages and visit them # in turn to get all the options data. # my $self = shift; my $expirations = shift || -1; # how many expirations to retrieve, < 0 means all my $q = LWP::UserAgent->new( agent => 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4) Gecko/20030624', timeout => 60, ); #Set proxy if user has provided one $q->proxy(['http'], $self->proxy) if $self->proxy; return unless $self->symbol; my $sym = uc $self->symbol; my $response = $q->get("http://finance.yahoo.com/q/op?s=$sym"); #Copy the LWP status to this instance $self->{success} = $response && $response->is_success; return unless $self->{success}; $self->{response} = $response; $self->{status} = $response->code; my $content = $response->content; return if $content =~ /there are no all markets results for/i; # MDER.PK my $tnum; my $st = HTML::TokeParser->new(\$content); my $ret; my $text; local ($_,$1,$2,$3,$4,$5); #Localizing special variables is recommended under mod_perl # # First look at the DIV tags to find 'View By Expiration'. Parse out # the list of expiration months. Create @optmonths containing expiration # months. Main loop will pop these off one by one, retrieve that page # and add the data to the data object. # my %month2num = qw(jan 01 feb 02 mar 03 apr 04 may 05 jun 06 jul 07 aug 08 sep 09 oct 10 nov 11 dec 12); my @optmonths = ('start'); #Hash to translate Yahoo's column headers to our standard hash keys my %xheaders = ( strike => 'strike', symbol => 'symbol', bid => 'bid', ask => 'ask', last => 'last', vol => 'volume', open_int => 'open', chg => 'change' ); my $expdate = ''; # @{$calldata} and @{$putdata} are arrays of hashes my $calldata = []; my $putdata = []; MAIN: while (@optmonths and $expirations) { # keeps looping if $expirations was -1 if ($optmonths[0] eq 'start') { #First time here, we're on the main query page. Extract expirations #months and populate @optmonths $expirations--; GETEXP: while ($st->get_tag('div')) { $text=$st->get_trimmed_text('/div'); if ($text =~ /view by expiration/i) { #Get expiration months my ($exp) = $text =~ /view by expiration(.*)call options/i; @optmonths = split(/\|/,$exp); #Convert 'Jan 01' format to 'YYYY-MM' for (@optmonths) { last unless /(\w{3})\s+(\d{2,4})/; $_ = ($2 < 100 ? 2000+$2 : $2) . '-' . $month2num{lc $1}; }; shift @optmonths; #The first month is the page we're already at last GETEXP; } } } else { #@optmonths has been populated, shift off the next month #and retrieve that page. When @optmonths is empty, we're done. #Additional months are at http://finance.yahoo.com/q/op?s=DIA&m=2007-06 my $month = shift @optmonths; $expirations--; $response = $q->get("http://finance.yahoo.com/q/op?s=$sym&m=$month"); $expdate = ''; #Copy the LWP status to this instance $self->{success} = $response->is_success; $self->{status} = $response->code; $self->{response} = $response; next MAIN unless $self->{success}; } # There's something like 25 or 26 tables present. We're only looking for # four of them: the Calls header and data tables and the Puts header # and data tables. # # We'll use HTML::TokeParser's ability to go from tag to tag # even though the rows might be in different tables. # This requires a specific order of tables: calls header then # calls data then puts header then puts data. # # Look at the first TD cell in a table to determine if it's one we want: # 'Call Options' is the header table for calls and # 'Put Options' is the header table for puts. The *next* table after the # header table that starts with 'Strike' is the data table for that # category. Use $mode to tell which table we're currently looking for. #Reset the TokeParser object so we can scan by tables $content = $response->content; $st = HTML::TokeParser->new(\$content); my ($tag,$newrow,$colcnt) = ('',0,0); my @callheaders = (); my @putheaders = (); $calldata = []; $putdata = []; $st->get_tag('table'); #Jump to first table my $mode='start'; ROW: while ($tag=$st->get_tag('tr','/table','/html')) { #TokeParser returns arrayref if found, undef if no more tags $tag = $tag->[0]; last MAIN if $tag =~ /\/html/i or not $tag; #Finished when getting put data but found end of table #Some options pages (e.g. CENTA) have no Puts, just the header line last ROW if $tag =~ /\/table/i and $mode =~ /gputdata/; #First loop: Getting Rows $newrow=1; CELL: while ($tag=$st->get_tag('td','/tr','/html')) { #Second loop: getting table cells my $in_the_money = 0+ (ref $tag->[1] && exists $tag->[1]->{class} && $tag->[1]->{class} eq 'yfnc_h'); $tag = $tag->[0]; last MAIN if $tag =~ /\/html/i; #No data returned last CELL if $tag =~ /\/tr/i; #last cell in row $text=$st->get_trimmed_text('/td'); #Perform cleanup & set mode between new rows if ($newrow) { if ($mode =~ /start|gcalldata/ and $text =~ /call options|put options/i) { #Found Header Table $mode='gcalldate' if $text =~ /call options/i; $mode='gputdate' if $text =~ /put options/i; $newrow=0; next CELL; } elsif ($mode eq 'gcalldate') { #Got the expiration date in the call header $mode = 'gcallheaders'; next ROW; } elsif ($mode eq 'gputdate') { #Got the expiration date in the put header $mode = 'gputheaders'; next ROW; } elsif (($mode eq 'gcallheaders' and not @callheaders) or ($mode eq 'gputheaders' and not @putheaders)) { #Haven't found column headers yet next ROW unless $text =~ /strike/i; } elsif ($mode eq 'gcalldata' or ($mode eq 'gcallheaders' and @callheaders)) { #Have column headers next ROW unless $text; #Nothing in first cell #Add a new row to @{$calldata} push @{$calldata}, {in_the_money => $in_the_money}; $mode='gcalldata'; $colcnt=0; } elsif ($mode eq 'gputdata' or ($mode eq 'gputheaders' and @putheaders)) { #Have column headers #Add a new row to @{$putdata} push @{$putdata}, {in_the_money => $in_the_money}; $mode = 'gputdata'; $colcnt=0; } else { #Nothing we want in this row next ROW; } } $newrow = 0; #Extract the data if ($mode =~ /gcalldate|gputdate/) { if ($text and not $expdate) { #Extract expiration date, convert to YYYYMMDD $text =~ /(\w{3})\s+(\d{1,2}),\s+(\d{4})/; $expdate = $3 . $month2num{lc $1} . $2; } $mode = 'gcallheaders' if $mode eq 'gcalldate'; $mode = 'gputheaders' if $mode eq 'gcalldate'; } elsif ($mode =~ /gcallheaders|gputheaders/) { #Extract table headers #Use %xheaders to translate to our standard headers $text =~ s/ /_/g; #Spaces to underscores push @callheaders, $xheaders{lc($text)} if $mode eq 'gcallheaders'; push @putheaders, $xheaders{lc($text)} if $mode eq 'gputheaders'; } elsif ($mode =~ /gcalldata|gputdata/) { #cleanup $text $text =~ s/,//g; #Remove commas if ($text =~ /(up|down)\s+(\d*.?\d*)/i) { #This is the Chg column #Convert 'Up/Down' to + or - $text = $2; $text*=-1 if $1=~/down/i; } #Insert the data #Remove the '.X' Yahoo appends to symbol if ($mode eq 'gcalldata') { $calldata->[-1]->{$callheaders[$colcnt]} = $text; $calldata->[-1]->{symbol} =~ s/\.X$//i if $callheaders[$colcnt] eq 'symbol'; } else { $putdata->[-1]->{$putheaders[$colcnt]} = $text; $putdata->[-1]->{symbol} =~ s/\.X$//i if $putheaders[$colcnt] eq 'symbol'; } $colcnt++; } } #Getting TD } #Getting TR #Sort calls and puts by strike price if (not exists $calldata->[0]->{symbol}) { # if the option has no calls, empty the array $calldata = []; } if (not exists $putdata->[0]->{symbol}) { # if the option has no puts (e.g. CENTA as of 2009-Feb-15), empty the array $putdata = []; } @{$calldata} = sort { $a->{strike} <=> $b->{strike} } @{$calldata}; @{$putdata} = sort { $a->{strike} <=> $b->{strike} } @{$putdata}; #If this expiration already exists in $self->{data}, append #new data and resort, otherwise create new expiration CHECKDUP: { foreach (@{$self->{data}}) { if ($_->{exp} == $expdate) { #Duplicate present @{$_->{calls}} = sort { $a->{strike} <=> $b->{strike} } (@{$_->{calls}}, @{$calldata}); @{$_->{puts}} = sort { $a->{strike} <=> $b->{strike} } (@{$_->{puts}}, @{$putdata}); last CHECKDUP; #Don't add new expiration } #Duplicate expiration already present } #Add new expiration #Only executed if no duplicates expirations present push @{$self->{data}}, { exp => $expdate, calls => $calldata, puts => $putdata }; } #Sort data by expirations @{$self->{data}} = sort { $a->{exp} <=> $b->{exp} } @{$self->{data}}; } #End MAIN loop } #End getyahoodata #------------------------------------------------------------------- sub getcboedata { require WWW::Mechanize; # # Main query page: # http://www.cboe.com/DelayedQuote/QuoteTable.aspx # # Get expirations from # http://www.cboe.com/DelayedQuote/SimpleQuote.aspx?ticker=BQQ+OH-E # # Unlike Yahoo, the main query page has *all* the options available. # Alas, it is lacking the expiration dates for those options. # We'll drill down into the individual option page to get the date. # # Right now we only do this once for each 'YY MMM' format date found # in the option description on the first page. We *assume* that all # subsequent dates of the same format have the *same* full date. # my $self = shift; my $q = WWW::Mechanize->new(autocheck => 0); $q->agent_alias('Linux Mozilla'); $q->quiet(1); $q->timeout(60); #Set proxy if user has provided one $q->proxy(['http', 'ftp'], $self->proxy) if $self->proxy; return unless $self->symbol; my $sym = uc $self->symbol; #Hash to translate CBOE column headers to our standard hash keys my %xheaders = ( bid => 'bid', ask => 'ask', last_sale => 'last', vol => 'volume', open_int => 'open', net => 'change' ); $q->get("http://www.cboe.com/DelayedQuote/QuoteTable.aspx"); return unless $q->success; $q->submit_form( fields => { 'ucQuoteTableCtl:txtSymbol' => $sym, 'ucQuoteTableCtl:ALL' => 2 }, button => 'ucQuoteTableCtl:btnSubmit' ); #Copy the WWW::Mechanize status to this instance $self->{success} = $q->success; $self->{status} = $q->status; $self->{response} = $q->response; return unless $q->success; # Output from mech-dump to get labels above: # ucQuoteTableCtl:txtSymbol= (text) # ucQuoteTableCtl:chkAllExchange= (checkbox) # [*/off|on/All exchange option quotes (if multiply listed)] # ucQuoteTableCtl:ALL=0 (radio) # [*0/List near term at-the-money options & Weeklys if avail.| # 2/List all options, LEAPS & Weeklys if avail. (Single page)] # ucQuoteTableCtl:btnSubmit=Submit (submit) my $tnum; my $st = HTML::TokeParser->new(\$q->{content}); my $ret; my ($tag,$text,$colcnt) = ('','',0); local ($_,$1,$2,$3,$4,$5); #Localizing special variables is recommended under mod_perl my @optmonths = (); my %months2num = qw(jan 01 feb 02 mar 03 apr 04 may 05 jun 06 jul 07 aug 08 sep 09 oct 10 nov 11 dec 12); my @callheaders = (); my @putheaders = (); my $putscol = 0; #Column where puts data starts $st->get_tag('table'); #Jump to first table #Find start of data: HEADER: while ($st->get_tag('tr')) { $st->get_tag('td'); if ($st->get_trimmed_text('/td') =~ /calls/i) { #Parse out the column headers my $mode='calls'; while (my $tag=$st->get_tag('td','/tr')) { #get_tag returns undef when no more tags $tag=@{$tag}[0]; last HEADER if $tag =~ /\/tr/i; my $text = $st->get_trimmed_text('/td'); $text =~ s/ /_/g; #spaces to underscores if ($text =~ /puts/i) { $mode = 'puts'; next; } if ($mode eq 'calls') { push @callheaders,$xheaders{lc $text}; } else { push @putheaders,$xheaders{lc $text}; } } last HEADER; } } #Unlike Yahoo, the main page does not have the actual #expiration date on it, just the YYMMM version. We are #going to *assume* that all YYMMM expirations are the #*same* actual date. The first time we hit a YYMMM date, #drill down into the details for that option to extract #the actual date and then use it for all subsequent #YYMMM options. #So, there might be a problem if there are weeklys, #monthlies or quarterlies present... #http://www.cboe.com/micro/weeklys/introduction.aspx my %expirations = (); my %tempdata = (); no warnings; ROW: while ($tag=$st->get_tag('tr','/table')) { #get_tag returns undef when no more tags $tag=@{$tag}[0]; last ROW if $tag =~ /\/table/; my $mode = 'start'; my @tmpheaders = @callheaders; my $call = {}; my $put = {}; my $exp = ''; CELL: while ($tag=$st->get_tag('td','/tr')) { $tag=@{$tag}[0]; last CELL if $tag =~ /\/tr/i; $text=$st->get_trimmed_text('/td'); next ROW if $text =~ /\[img\]/i; #There's an IMG after the column headers #Description looks like "07 May 57.00 (IWT EE-E)" if ($mode eq 'start' and $text =~ /(\d{2} \w{3}) (\d{1,5}\.\d{2}) \((\w{1,4}) (\w{2})-(\w)\)/) { #Found call description $exp = $1; $call->{strike} = $2; $call->{symbol} = "$3$4"; my $linksym = "$3+$4"; my $type = $5; $exp =~ s/ //g; #Back-referencing variables reset on any regex #Check if expiration date has already been found, if not #drill down to option detail page to get it unless ($expirations{$exp}) { my $det = WWW::Mechanize->new(); $det->agent_alias('Linux Mozilla'); $det->quiet(1); $det->get("http://www.cboe.com/DelayedQuote/SimpleQuote.aspx?ticker=$linksym-$type"); #Copy the WWW::Mechanize status to this instance $self->{success} = $det->success; $self->{status} = $det->status; $self->{response} = $det->response; my $dat = HTML::TokeParser->new(\$det->{content}); unless ($self->{success}) { #Detail lookup failed. IP address probably blacklisted #Manually calc 3rd Friday of month my ($tyear,$tmon) = $exp =~ /(\d{2})(\w{3})/; $tyear += 2000; $tmon = lc $tmon; my %mon2digit = qw/jan 01 feb 02 mar 03 apr 04 may 05 jun 06 jul 07 aug 08 sep 09 oct 10 nov 11 dec 12/; use Date::Calc; #DOW is 5 for Friday, 3rd occurance my ($year,$month,$day) = Date::Calc::Nth_Weekday_of_Month_Year($tyear,$mon2digit{$tmon},5,3); #Pad zeros to month and day $month = substr(100+$month,-2); $day = substr(100+$day,-2); $expirations{$exp} = "$year$month$day"; } else { #Extract date from option detail page DATETABLE: while (my $tag=$dat->get_tag('table', '/table')) { $tag=@{$tag}[0]; next DATETABLE if $tag =~ /\/table/i; my $text=$dat->get_trimmed_text('/table'); if ($text =~ /expiration date\s*(\d{2})\/(\d{2})\/(\d{4})/i) { $expirations{$exp} = "$3$1$2"; last DATETABLE; } } } } $mode = 'call'; } elsif ($mode eq 'call' and $text =~ /(\d{2} \w{3}) (\d{1,5}\.\d{2}) \((\w{1,4}) (\w{2})-\w\)/) { #Found put description $exp = $1 unless $exp; #Should have found it with call $put->{strike} = $2; $put->{symbol} = "$3$4"; $exp =~ s/ //g; #Back-referencing variables reset on any regex $mode = 'put'; @tmpheaders = @putheaders; } elsif ($mode eq 'call') { $call->{shift @tmpheaders} = $text; } elsif ($mode eq 'put') { $put->{shift @tmpheaders} = $text; } else { #This should never happen #print "ERROR parsing CBOE data!!!!\nText: $text\n"; } } #Get TD #Move put and call to proper location using $exp unless ($tempdata{$exp}->{exp}) { #Create new expiration in %tempdata $tempdata{$exp}->{exp} = $expirations{$exp}; $tempdata{$exp}->{calls} = []; $tempdata{$exp}->{puts} = []; } #Move hashrefs into %tempdata push @{$tempdata{$exp}->{calls}},$call; push @{$tempdata{$exp}->{puts}},$put; } #Get TR #Sort %tempdata by expiration dates and move into @{$self->{data}} push @{$self->{data}}, $tempdata{$_} for sort { $tempdata{$a}->{exp} <=> $tempdata{$b}->{exp} } keys %tempdata; #Sort puts and calls at each expiration by strike price for (@{$self->{data}}) { @{$_->{calls}} = sort { $a->{strike} <=> $b->{strike} } @{$_->{calls}}; @{$_->{puts}} = sort { $a->{strike} <=> $b->{strike} } @{$_->{puts}}; } } #End getcboedata 1; __END__ =head1 NAME Finance::QuoteOptions - Perl extension for retrieving options pricing and series information from the web. =head1 SYNOPSIS use Finance::QuoteOptions; my $q=Finance::QuoteOptions->new('DIA'); die 'Retrieve Failed' unless $q->retrieve; #Expiration dates in ISO format (YYYYMMDD) my @expirations = @{$q->expirations}; #Calls/Puts for next expiration, sorted by strike price my @calls = @{$q->calls(0)}; my @puts = @{$q->puts(0)}; #Data from an individual option my $strike = $q->option('XYZAB')->{strike}; my $symbol = $q->option('XYZAB')->{symbol}; my $bid = $q->option('XYZAB')->{bid}; my $ask = $q->option('XYZAB')->{ask}; =head1 DESCRIPTION A 'screen-scraper' utility using C and C to retrieve and parse options information from either Yahoo Finance or the Chicago Board Options Exchange (CBOE) web site. The CBOE probably has better data but Yahoo is much faster. By default, Yahoo Finance is used as the source. The Yahoo address used, using DIA as an example, is: http://finance.yahoo.com/q/op?s=DIA The CBOE address used is: http://www.cboe.com/DelayedQuote/QuoteTable.aspx =head2 CBOE Blacklist Warning The CBOE site has two interfaces for retrieving option information: a page where you can download a CSV file and a web page that displays an HTML table. At the time of writing this module (2007-2008), there was a big warning not to do any kind of automated retrieval of the CSV information. So I wrote this module to use the web page, even though the CSV file would be I simple to process. Unfortunately, although I could find no such warning on the web portion of the CBOE site, I B got blacklisted from using the web portion, after a couple of weeks of using this code and tens of thousands of queries. I was blacklisted on June 4, 2007. Sometime before February 2009 the blacklist was removed. However I assume the block would be replaced if this code is used again. As of February 2009, the HTML page also has the warning from the CSV page. So if you're doing many queries, use Yahoo as your source. Yahoo is much faster anyway. It does have less data than CBOE, but the only difference I've seen were options with zero volume and open interest. Give it a try for 'KB'. =head2 Methods The following methods are available: =over 4 =item my $q = Finance::QuoteOptions->new; =item my $q = Finance::QuoteOptions->new('DIA'); The first version creates the new object but doesn't set the target symbol. Use C to set the target symbol. The second version creates the new object and sets the target symbol in one step. =item source; =item source('yahoo'); =item source('cboe'); Sets or retrieves the current data source. The default is Yahoo Finace. Only acceptable options are C and C. Submitting anything else will set the source to C. Always returns currently selected source. =item symbol; =item symbol('DIA'); Sets or retrieves the target symbol for the query. =item proxy; =item proxy('http://proxy.example.com:8000/'); Sets or retrieves the proxy setting for the query. Set this before issuing a retrieve command. This is currently untested. If you are behind a proxy, please email the author if you use this feature and let him know how it works. =item retrieve; =item retrieve(N); Retrieves data from designated source, for N expiration dates starting from now. Retrieves all available expiration dates if N is not specified. If you are only interested in the upcoming expiration, call << $q->retrieve(1) >> as this will be a few times faster. Returns L value for last http access. Note that L does I indictate whether there are options available for the queried stock. To make that determination, check L after issuing a L to see if any options have been retrieved. For example: $q->retrieve; if (@{$q->expirations}) { #There are options } else { #There are no options } =item expirations; Returns arrayref of all expiration dates in the format YYYYMMDD sorted by date. Returns C if not found. =item calls(0); =item calls(200705); =item calls(20070518); =item puts(0); =item puts(200705); =item puts(20070518); Returns an arrayref containing all calls or puts for a given expiration, sorted by strike price. Returns C if expiration is not found. Parameter can take three forms: =over 4 =item * Number of Expirations Out Parameter '0' is the next expiration, '1' is two expirations out and so on. =item * Integer YYYYMM. If there happen to be two expirations in the same month, only the first will be returned. Use L to check for multiple expirations. See L< Notes> for more information about multiple expirations. =item * Integer YYYYMMDD. This specifies the exact expiration date. =back =item The arrayref is an array of hashes containing all calls or puts for a given expiration. The hashes referenced within the array look like: { strike => 000.00, symbol => 'ABCD', bid => 000.00, ask => 000.00, last => 000.00, open => 0000, volume => 0000, change => 0000, in_the_money => 1 } Returns C if not found. The in_the_money key is only found if using Yahoo as a data source. It is not set when using the CBOE. in_the_money contains either 0 or 1. =item callsymbols(0); =item putsymbols(0); Returns an arrayref of call or put symbols for a given expiration. Parameter is the number of expirations out starting from zero. Returns C if not found. =item callstrikes(0); =item putstrikes(0); Returns an arrayref of call or put strike prices for a given expiration. Parameter is the number of expirations out starting from zero. Returns C if not found. =item option('ABCD'); Returns hashref with all of the data for a single option symbol. If a symbol has somehow been duplicated, the nearest symbol by date will be returned. Parameter is case-insensitive. The hash has the same structure as C or C above with the addition of a C key containing the expiration date. Returns C if not found. =item data() Returns arrayref containing all data retrieved. See L below. =item success() =item response() =item status() All three are directly copied from the C object. See it's documentation for more details. Retrieving full options data for a symbol requires multiple http requests. Only the I request will be reported here. C Returns a boolean telling whether the last request was successful. If there hasn’t been an operation yet, returns false. This does I indicate if options are available for a stock. See L above. C Return the current response as an C object. C Returns the HTTP status code of the response. =item version(); Returns Finance::QuoteOption version. =back =head2 Internal Data Structure The methods provided will slice and dice the options data in various ways. However, the data is maintained in a single data structure that can be directly accessed. Yes, this is bad OO practice, but hey, I think someone once said there's more than one way to do it. Everything is stored in an arrayref retrieved by the L method. Each array element is a hashref. Each referenced hash has the keys C, C and C: @data ( \%expiration1 { exp => YYYYMMDD, calls => \@calldata, puts => \@putdata }, \%expiration2 { exp => YYYYMMDD, calls => \@calldata, puts => \@putdata }, ... ) The arrays referenced by C and C are each arrays of hashrefs. The final hashes contain all the data for an individual option: @callorputdata ( \%option1 { strike => 000.00, symbol => 'ABCD', bid => 000.00, ask => 000.00, last => 000.00, open => 0000, volume => 0000, change => 0000, in_the_money => 1 }, \%option2 { ...ditto... }, ... ) So, to enumerate all available expiration dates: print $_->{exp},"\n" foreach @{$q->data}; Or to display the number of puts and calls along with the symbol and strike price of the first and last call options of each expiration: foreach (@{$q->data}) { print "\n",$_->{exp},":\n"; print 'Calls: ', scalar @{$_->{calls}},"\n"; print 'Puts: ', scalar @{$_->{puts}},"\n"; print 'First Call: ', $_->{calls}->[0]->{symbol}, " Strike $_->{calls}->[0]->{strike} \n"; print 'Last Call: ', $_->{calls}->[-1]->{symbol}, " Strike $_->{calls}->[-1]->{strike} \n"; } =head2 Notes C and C each have their own complex set of dependencies. So be prepared for a wait if doing a CPAN install on a basic Perl distribution. Be sure to read L. C's ability to jump from tag to tag should make this code impervious to web page additions or changes which surround the actual options information we're after. That is, as long as the structure of the tables containing the basic information doesn't change. The CBOE site is I slower than Yahoo. In my testing Yahoo took about one second to retrieve all options for a stock whereas the CBOE site took five to fifteen seconds. Because of the way the CBOE site is structured, there might be a problem if there are options with more than one expiration date in a single month. right now, they'll all end up in the same expiration. I could fix this by drilling down into the details for I option but then the CBOE retrieval would get even slower. So be careful if weeklys, monthlies or quarterlies are available for a stock. See L for more information. Feel free to contact me at the address below if you have any questions, problems or suggestions. =head2 EXPORT None by default. =head1 SEE ALSO L L L L L L =head1 AUTHOR Kirk Bocek, Equoteoptions EATE kbocek.comE Version 0.20 contributions by Dan Descaleau, Eddascalescu EATE gmail.comE =head1 COPYRIGHT AND LICENSE Copyright (C) 2007,2008,2009 by Kirk Bocek This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.5 or, at your option, any later version of Perl 5 you may have available. =cut