2017-12-21 10:54:49 -06:00
|
|
|
#!@PERL@ -w
|
2001-03-16 16:49:12 -06:00
|
|
|
######################################################################
|
2006-06-27 19:02:30 -05:00
|
|
|
### gnc-fq-helper - present a scheme interface to Finance::Quote
|
2001-03-16 16:49:12 -06:00
|
|
|
### Copyright 2001 Rob Browning <rlb@cs.utexas.edu>
|
2015-07-07 15:02:18 -05:00
|
|
|
###
|
|
|
|
### 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 2 of
|
|
|
|
### the License, 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.
|
|
|
|
###
|
2001-03-16 16:49:12 -06:00
|
|
|
### You should have received a copy of the GNU General Public License
|
|
|
|
### along with this program# if not, contact:
|
|
|
|
###
|
|
|
|
### Free Software Foundation Voice: +1-617-542-5942
|
2005-11-16 23:35:02 -06:00
|
|
|
### 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
|
|
|
|
### Boston, MA 02110-1301, USA gnu@gnu.org
|
2001-03-16 16:49:12 -06:00
|
|
|
######################################################################
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use English;
|
|
|
|
use FileHandle;
|
|
|
|
|
2015-07-07 15:02:18 -05:00
|
|
|
# Date::Manip provides ParseDate, ParseDateString, and UnixTime.
|
2006-07-01 10:05:53 -05:00
|
|
|
use Date::Manip;
|
|
|
|
|
2012-02-15 05:22:43 -06:00
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
gnc-fq-helper - allows gnucash to communicate with Finance::Quote
|
|
|
|
over pipes from guile. The requests and responses
|
|
|
|
are scheme forms.
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
gnc-fq-helper
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
Input: (on standard input - one entry per line and one line per
|
|
|
|
entry, and double quotes must only be delimiters, not string
|
|
|
|
content -- remember, we don't have a real scheme parser on the perl
|
|
|
|
side :>).
|
|
|
|
|
|
|
|
(<method-name> symbol symbol symbol ...)
|
|
|
|
|
|
|
|
where <method-name> indicates the desired Finance::Quote method.
|
2018-01-02 12:43:49 -06:00
|
|
|
One can list the many methods by running gnc-fq-check.
|
2012-02-15 05:22:43 -06:00
|
|
|
|
|
|
|
For currency quotes, the symbols alternate between the 'from'
|
|
|
|
and 'to' currencies.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
2018-01-02 12:43:49 -06:00
|
|
|
(alphavantage "IBM" "LNUX")
|
2012-02-15 05:22:43 -06:00
|
|
|
(fidelity_direct "FBIOX" "FSELX")
|
|
|
|
(currency "USD" "AUD")
|
|
|
|
|
|
|
|
Output (on standard output, one output form per input line):
|
|
|
|
|
|
|
|
Schemified version of gnc-fq's output, basically an alist of
|
|
|
|
alists, as in the example below. Right now, only the fields that
|
|
|
|
this script knows about (and knows how to convert to scheme) are
|
|
|
|
returned, so the conversion function will have to be updated
|
|
|
|
whenever Finance::Quote changes. Currently you'll get symbol,
|
|
|
|
gnc:time-no-zone, and currency, and either last, nav, or price.
|
|
|
|
Fields with gnc: prefixes are non-Finance::Quote fields.
|
|
|
|
gnc:time-no-zone is returned as a string of the form "YYYY-MM-DD
|
|
|
|
HH:MM:SS", basically the unmolested (and underspecified) output of
|
|
|
|
the quote source. It's up to you to know what it's proper timezone
|
|
|
|
really is. i.e. if you know the time was in America/Chicago, you'll
|
|
|
|
need to convert it to that.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
2018-01-02 12:43:49 -06:00
|
|
|
$ echo '(alphavantage "CSCO" "JDSU" "^IXIC")' | ./gnc-fq-helper
|
2012-02-15 05:22:43 -06:00
|
|
|
(("CSCO" (symbol . "CSCO")
|
|
|
|
(gnc:time-no-zone . "2001-03-13 19:27:00")
|
|
|
|
(last . 20.375)
|
|
|
|
(currency . "USD"))
|
|
|
|
("JDSU" (symbol . "JDSU")
|
|
|
|
(gnc:time-no-zone . "2001-03-13 19:27:00")
|
|
|
|
(last . 23.5625)
|
|
|
|
(currency . "USD"))
|
|
|
|
("^IXIC" (symbol . ^IXIC)
|
|
|
|
(gnc:time-no-zone . 2002-12-04 17:16:00)
|
|
|
|
(last . 1430.35)
|
|
|
|
(currency . failed-conversion)))
|
|
|
|
|
|
|
|
On error, the overall result may be #f, or on individual errors, the
|
|
|
|
list sub-item for a given symbol may be #f, like this:
|
|
|
|
|
2018-01-02 12:43:49 -06:00
|
|
|
$ echo '(alphavantage "CSCO" "JDSU")' | ./gnc-fq-helper
|
2012-02-15 05:22:43 -06:00
|
|
|
(#f
|
|
|
|
("JDSU" (symbol . "JDSU")
|
|
|
|
(gnc:time-no-zone . "2001-03-13 19:27:00")
|
|
|
|
(last . 23.5625)
|
|
|
|
(currency . "USD")))
|
|
|
|
|
|
|
|
further, errors may be stored with each quote as indicated in
|
|
|
|
Finance::Quote, and whenever the conversion to scheme data fails,
|
|
|
|
the field will have the value 'failed-conversion, and accordingly
|
|
|
|
this symbol will never be a legitimate conversion.
|
|
|
|
|
|
|
|
Exit status
|
|
|
|
|
|
|
|
0 - success
|
|
|
|
non-zero - failure
|
|
|
|
|
|
|
|
=cut
|
2001-03-16 16:49:12 -06:00
|
|
|
|
2015-07-26 15:11:45 -05:00
|
|
|
# Set a base date with the current time in the current TZ:
|
|
|
|
my $base_date = new Date::Manip::Date;
|
|
|
|
$base_date->parse("now");
|
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
# The methods we know about. For now we assume they all have the same
|
|
|
|
# signature so this works OK.
|
|
|
|
|
2001-03-30 06:43:11 -06:00
|
|
|
sub check_modules {
|
2015-07-07 15:02:18 -05:00
|
|
|
my @modules = qw(Finance::Quote Date::Manip);
|
2001-03-30 06:43:11 -06:00
|
|
|
my @missing;
|
|
|
|
|
|
|
|
foreach my $mod (@modules) {
|
|
|
|
if (eval "require $mod") {
|
|
|
|
$mod->import();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
push (@missing, $mod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return unless @missing;
|
|
|
|
|
2001-03-30 07:09:22 -06:00
|
|
|
print STDERR "\n";
|
|
|
|
print STDERR "You need to install the following Perl modules:\n";
|
2001-03-30 06:43:11 -06:00
|
|
|
foreach my $mod (@missing) {
|
2001-03-30 07:09:22 -06:00
|
|
|
print STDERR " ".$mod."\n";
|
2001-03-30 06:43:11 -06:00
|
|
|
}
|
|
|
|
|
2001-03-30 07:09:22 -06:00
|
|
|
print STDERR "\n";
|
2006-06-27 19:02:30 -05:00
|
|
|
print STDERR "Use your system's package manager to install them,\n";
|
|
|
|
print STDERR "or run 'gnc-fq-update' as root.\n";
|
2001-03-30 06:43:11 -06:00
|
|
|
|
2001-04-16 04:16:52 -05:00
|
|
|
print "missing-lib";
|
|
|
|
|
|
|
|
exit 1;
|
2001-03-30 06:43:11 -06:00
|
|
|
}
|
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
sub schemify_string {
|
|
|
|
my($str) = @_;
|
|
|
|
|
|
|
|
if(!$str) { return "failed-conversion"; }
|
|
|
|
|
|
|
|
# FIXME: Is this safe? Can we just double all backslashes and backslash
|
|
|
|
# escape all double quotes and get the right answer?
|
|
|
|
|
|
|
|
# double all backslashes.
|
|
|
|
my $bs = "\\";
|
|
|
|
$str =~ s/$bs$bs/$bs$bs/gmo;
|
|
|
|
|
|
|
|
# escape all double quotes.
|
|
|
|
# Have to do this because the perl-mode parser freaks out otherwise.
|
|
|
|
my $dq = '"';
|
|
|
|
$str =~ s/$dq/$bs$dq/gmo;
|
|
|
|
return '"' . $str . '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
sub schemify_boolean {
|
|
|
|
my($bool) = @_;
|
|
|
|
|
|
|
|
if($bool) {
|
|
|
|
return "#t";
|
|
|
|
} else {
|
|
|
|
return "#f";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub schemify_num {
|
|
|
|
my($numstr) = @_;
|
|
|
|
# This is for normal numbers, not the funny ones like "2.346B".
|
|
|
|
# For now we don't need to do anything.
|
|
|
|
|
|
|
|
if(!$numstr) { return "failed-conversion"; }
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2014-11-23 22:28:26 -06:00
|
|
|
if($numstr =~ /^\s*(\d+(\.\d+)?([eE][+-]?\d+)?)$/o) {
|
2019-05-14 15:34:07 -05:00
|
|
|
return "#e" . $1;
|
2001-03-16 16:49:12 -06:00
|
|
|
} else {
|
|
|
|
return "failed-conversion";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# sub schemify_range {
|
|
|
|
# #convert range in form ``num1 - num2'' to ``(num1 num2)''.
|
|
|
|
# }
|
|
|
|
|
|
|
|
sub get_quote_time {
|
|
|
|
# return the date.
|
|
|
|
my ($item, $quotehash) = @_;
|
|
|
|
|
|
|
|
my $datestr = $$quotehash{$item, 'date'};
|
|
|
|
my $timestr = $$quotehash{$item, 'time'};
|
2015-07-26 15:11:45 -05:00
|
|
|
my $format = "%Y-%m-%d %H:%M:%S";
|
2015-08-02 10:38:12 -05:00
|
|
|
my $result;
|
2015-07-26 15:11:45 -05:00
|
|
|
|
|
|
|
if ($datestr) {
|
|
|
|
my $parsestr = $datestr . " " . ($timestr ? $timestr : "12:00:00");
|
|
|
|
my $date = $base_date->new();
|
|
|
|
my $err = $date->parse($parsestr);
|
|
|
|
if ($err) {
|
2015-08-02 10:38:12 -05:00
|
|
|
print $date->err(), " $parsestr\n";
|
|
|
|
$result = $base_date->printf($format);
|
2015-07-26 15:11:45 -05:00
|
|
|
}
|
2015-08-02 10:38:12 -05:00
|
|
|
else {
|
|
|
|
$result = $date->printf($format);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$result = $base_date->printf($format);
|
2001-03-16 16:49:12 -06:00
|
|
|
}
|
2015-08-02 10:38:12 -05:00
|
|
|
return("\"$result\"");
|
2001-03-16 16:49:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
sub schemify_quote {
|
|
|
|
my($itemname, $quotehash, $indentlevel) = @_;
|
|
|
|
my $scmname = schemify_string($itemname);
|
|
|
|
my $quotedata = "";
|
|
|
|
my $field;
|
|
|
|
my $data;
|
|
|
|
|
|
|
|
if (!$$quotehash{$itemname, "success"}) {
|
|
|
|
return schemify_boolean(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
$field = 'symbol';
|
2002-12-06 02:42:02 -06:00
|
|
|
if (($$quotehash{$itemname, $field})) {
|
|
|
|
$data = schemify_string($$quotehash{$itemname, $field});
|
|
|
|
} else {
|
2003-01-21 19:23:57 -06:00
|
|
|
# VWD and a few others don't set the symbol field
|
|
|
|
$data = schemify_string($itemname);
|
2002-12-06 02:42:02 -06:00
|
|
|
}
|
2001-03-16 16:49:12 -06:00
|
|
|
$quotedata .= "($field . $data)";
|
|
|
|
|
|
|
|
$field = 'gnc:time-no-zone';
|
|
|
|
$data = get_quote_time($itemname, $quotehash);
|
2001-05-08 04:36:40 -05:00
|
|
|
$quotedata .= " ($field . $data)" if $data;
|
2001-03-16 16:49:12 -06:00
|
|
|
|
|
|
|
$field = 'last';
|
2001-05-01 01:52:35 -05:00
|
|
|
if (!($$quotehash{$itemname, $field})) {
|
|
|
|
$field = 'nav';
|
|
|
|
}
|
|
|
|
if (!($$quotehash{$itemname, $field})) {
|
|
|
|
$field = 'price';
|
|
|
|
}
|
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
$data = schemify_num($$quotehash{$itemname, $field});
|
|
|
|
$quotedata .= " ($field . $data)";
|
|
|
|
|
|
|
|
$field = 'currency';
|
|
|
|
$data = schemify_string($$quotehash{$itemname, $field});
|
|
|
|
$quotedata .= " ($field . $data)";
|
|
|
|
|
|
|
|
return "($scmname $quotedata)";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub schemify_quotes {
|
|
|
|
my($symbols, $quotehash) = @_;
|
|
|
|
my $resultstr = "";
|
|
|
|
my $sym;
|
|
|
|
my $separator = "";
|
|
|
|
|
|
|
|
# we have to pass in @$items because Finance::Quote just uses the
|
|
|
|
# mangled "$name$field string as the key, so there's no way (I know
|
|
|
|
# of) to find out which stocks are in a given quotehash, just given
|
|
|
|
# the quotehash.
|
|
|
|
|
|
|
|
foreach $sym (@$symbols) {
|
|
|
|
$resultstr .= $separator . schemify_quote($sym, $quotehash, 2);
|
|
|
|
if(!$separator) { $separator = "\n "; }
|
|
|
|
}
|
|
|
|
return "($resultstr)\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub parse_input_line {
|
|
|
|
|
|
|
|
# FIXME: we need to rewrite parsing to handle commands modularly.
|
|
|
|
# Right now all we do is hard-code "fetch".
|
|
|
|
|
|
|
|
my($input) = @_;
|
|
|
|
# Have to do this because the perl-mode parser freaks out otherwise.
|
|
|
|
my $dq = '"';
|
|
|
|
my @symbols;
|
|
|
|
|
2017-01-16 16:03:50 -06:00
|
|
|
# Make sure we have an opening ( preceded only by whitespace.
|
2001-03-16 16:49:12 -06:00
|
|
|
# and followed by a one word method name composed of [a-z_]+.
|
2002-12-05 01:19:46 -06:00
|
|
|
# Also allow the '.' and '^' characters for stock indices.
|
2001-03-16 16:49:12 -06:00
|
|
|
# Kill off the whitespace if we do and grab the command.
|
2002-12-05 01:19:46 -06:00
|
|
|
if($input !~ s/^\s*\(\s*([\.\^a-z_]+)\s+//o) { return 0; }
|
2001-03-16 16:49:12 -06:00
|
|
|
|
|
|
|
my $quote_method_name = $1;
|
|
|
|
|
|
|
|
# Make sure we have an ending ) followed only by whitespace
|
|
|
|
# and kill it off if we do...
|
|
|
|
if($input !~ s/\s*\)\s*$//o) { return 0; }
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
while($input) {
|
|
|
|
# Items should look like "RHAT"
|
|
|
|
# Grab RHAT and delete "RHAT"\s*
|
|
|
|
if($input !~ s/^$dq([^$dq]+)$dq\s*//o) { return 0; }
|
|
|
|
my $symbol = $1;
|
|
|
|
push @symbols, $symbol;
|
|
|
|
}
|
|
|
|
|
|
|
|
my @result = ($quote_method_name, \@symbols);
|
|
|
|
return \@result;
|
|
|
|
}
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
# Runtime.
|
|
|
|
|
2001-03-30 06:43:11 -06:00
|
|
|
# Check for and load non-standard modules
|
|
|
|
check_modules ();
|
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
# Create a stockquote object.
|
|
|
|
my $quoter = Finance::Quote->new();
|
2006-06-27 19:02:30 -05:00
|
|
|
my $prgnam = "gnc-fq-helper";
|
2001-03-16 16:49:12 -06:00
|
|
|
|
|
|
|
# Disable default currency conversions.
|
|
|
|
$quoter->set_currency();
|
|
|
|
|
|
|
|
while(<>) {
|
|
|
|
|
|
|
|
my $result = parse_input_line($_);
|
|
|
|
|
|
|
|
if(!$result) {
|
|
|
|
print STDERR "$prgnam: bad input line ($_)\n";
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
my($quote_method_name, $symbols) = @$result;
|
2001-03-18 02:23:42 -06:00
|
|
|
my %quote_data;
|
|
|
|
|
2002-12-07 18:21:25 -06:00
|
|
|
if($quote_method_name =~ m/^currency$/) {
|
|
|
|
my ($from_currency, $to_currency) = @$symbols;
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2003-10-24 01:23:23 -05:00
|
|
|
last unless $from_currency;
|
|
|
|
last unless $to_currency;
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2002-12-07 18:21:25 -06:00
|
|
|
my $price = $quoter->currency($from_currency, $to_currency);
|
2015-09-01 16:47:40 -05:00
|
|
|
my $inv_price = undef;
|
2019-01-27 22:44:36 -06:00
|
|
|
# Sometimes price quotes are available in only one direction.
|
|
|
|
unless (defined($price)) {
|
2015-09-01 16:47:40 -05:00
|
|
|
$inv_price = $quoter->currency($to_currency, $from_currency);
|
|
|
|
if (defined($inv_price)) {
|
|
|
|
my $tmp = $to_currency;
|
|
|
|
$to_currency = $from_currency;
|
|
|
|
$from_currency = $tmp;
|
|
|
|
$price = $inv_price;
|
|
|
|
}
|
|
|
|
}
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2006-06-13 19:04:59 -05:00
|
|
|
$quote_data{$from_currency, "success"} = defined($price);
|
2002-12-07 18:21:25 -06:00
|
|
|
$quote_data{$from_currency, "symbol"} = $from_currency;
|
|
|
|
$quote_data{$from_currency, "currency"} = $to_currency;
|
|
|
|
$quote_data{$from_currency, "last"} = $price;
|
2001-05-08 04:36:40 -05:00
|
|
|
|
2002-12-07 18:21:25 -06:00
|
|
|
my @new_symbols = ($from_currency);
|
|
|
|
$symbols = \@new_symbols;
|
|
|
|
} else {
|
|
|
|
%quote_data = $quoter->fetch($quote_method_name, @$symbols);
|
2001-03-16 16:49:12 -06:00
|
|
|
}
|
|
|
|
|
2007-01-18 23:53:30 -06:00
|
|
|
if (%quote_data) {
|
|
|
|
print schemify_quotes($symbols, \%quote_data);
|
|
|
|
} else {
|
2001-03-16 16:49:12 -06:00
|
|
|
print "#f\n";
|
|
|
|
}
|
2001-03-18 02:23:42 -06:00
|
|
|
|
2001-03-16 16:49:12 -06:00
|
|
|
STDOUT->flush();
|
|
|
|
}
|
|
|
|
|
|
|
|
exit 0;
|
|
|
|
|
|
|
|
__END__
|
|
|
|
|
|
|
|
# Keep this around in case we need to go back to complex per-symbol args.
|
|
|
|
#
|
|
|
|
# while($input) {
|
|
|
|
# # Items should look like "RHAT" "EST")
|
|
|
|
# # Grab RHAT and delete ("RHAT"\s*
|
|
|
|
# if($input !~ s/^\(\s*$dq([^$dq]+)$dq\s*//o) { return 0; }
|
|
|
|
# my $symbol = $1;
|
|
|
|
# my $timezone;
|
|
|
|
# # Now grab EST or #f and delete \s*"EST") or #f)
|
|
|
|
# if($input =~ s/^\s*$dq([^$dq]+)$dq\)\s*//o) {
|
|
|
|
# $timezone = $1;
|
|
|
|
# } else {
|
|
|
|
# if($input =~ s/^\s*(\#f)\)\s*//o) {
|
|
|
|
# $timezone = 0;
|
|
|
|
# } else {
|
|
|
|
# return 0;
|
2015-07-07 15:02:18 -05:00
|
|
|
# }
|
2001-03-16 16:49:12 -06:00
|
|
|
# }
|
|
|
|
|
|
|
|
# sub get_quote_utc {
|
|
|
|
# # return the date in utc epoch seconds, using $timezone if specified.
|
|
|
|
# my ($item, $timezone, $quotehash) = @_;
|
|
|
|
|
|
|
|
# if(!defined($timezone)) { return "failed-conversion"; }
|
|
|
|
|
|
|
|
# my $datestr = $$quotehash{$item, 'date'};
|
|
|
|
# my $timestr = $$quotehash{$item, 'time'};
|
|
|
|
|
|
|
|
# if(!$datestr) {
|
|
|
|
# return "failed-conversion";
|
|
|
|
# }
|
|
|
|
# my $parsestr = $datestr;
|
|
|
|
# if($timestr) {
|
|
|
|
# $parsestr .= " $timestr";
|
|
|
|
# }
|
|
|
|
|
|
|
|
# if($timezone) {
|
|
|
|
# # Perform a conversion.
|
|
|
|
# $parsestr = Date_ConvTZ(ParseDate($parsestr), $timezone, 'UTC');
|
|
|
|
# }
|
|
|
|
# my $result = UnixDate($parsestr, "%s");
|
|
|
|
# if($result !~ /^(\+|-)?\d+$/) {
|
|
|
|
# $result = "failed-conversion";
|
|
|
|
# }
|
|
|
|
# return $result;
|
|
|
|
# }
|
|
|
|
|
|
|
|
## Local Variables:
|
|
|
|
## mode: perl
|
|
|
|
## End:
|