An Extensible Viewer for your Personal FinancesThe Bottom LineMichael Schilli |
Contrary to popular belief, rich people are not unhappier than people without a penny to their names. You can hear the sighs of relief all over the country – gone are the fears of wealth-induced miserliness. And for the first time in years, people again dare to check their financial status. Read on to find out how.
With the exception of a few eccentrics who prefer to horde their wealth under the floorboards of their villas, people are increasingly turning to programs such as Gnucash to manage their accounts and depots. Account management programs help you tidy up your accounts, giving you neat formatting or even graphical output. This said, the open source tools are still very much in the vein of Quicken and Microsoft Money and entail a lot of highly disciplined work.
Amateur accountants typically lack the time to complete all those painstaking entries, not to mention the complex installation that Gnucash requires. In other words, this is not a simple tool and not easily extensible. In contrast to this, our Perl script is designed for the rest of us, who prefer not to spend more than ten minutes a month updating their bank balances but still like to check out the bottom line of their shareholdings on a daily basis.
The system supports modular plugin-based extensions, allowing users to take currency conversion or tax scenarios into consideration and providing customization without drifting toward bloatware.
It is so easy to overdo things. For example, it doesn’t make sense to develop a module for share splitting, which occurs once every few years; a few manual steps are all it takes to handle the split (in other words, don’t try to be everyone’s darling.) Our beancounter script tries to find the golden mean. It has basic functionality that gives users a sum total for multiple accounts and shareholdings, but it leaves enough scope for users to handle their own specialized requirements.
Listing 1: beancounter |
01 #!/usr/bin/perl -w 02 ############################# 03 # beancounter - Money 04 # Counting Interpreter 05 # Mike Schilli, 2004 06 # (m@perlmeister.com) 07 ############################# 08 use strict; 09 10 use lib 11 '/some/where/in/module/land'; 12 13 use Log::Log4perl qw(:easy); 14 Log::Log4perl->easy_init( 15 $ERROR); 16 17 use Plugger; 18 my $string = join '', <>; 19 20 my $plugger = Plugger->new(); 21 $plugger->init(); 22 $plugger->parse($string); |
Users can define their account data in a file called money, as shown in Figure 1. The account keyword defines a new account; a shareholding starts with stock; and cash is self-explanatory. The beancounter script in Listing 1 is the interpreter for this financial data. It parses the account definitions, ascertains share prices, and adds profits and losses to give you a financial statement. Typing beancounter money in the command line launches the script, but there is an even easier way. Make the money configuration file executable and add the beancounter interpreter to the she-bang line. In other words, money does not use perl as its interpreter, but beancounter.
If you do not use the hyper-modern Zsh shell, but use good old Bash instead, you will not be able to add a script to the she-bang line. Instead, you need a C wrapper, which is what the following C program, beancount.c, gives us:
main(int argc, char **argv) { execv("/usr/bin/U beancounter", argv); }
Now compile beancount.c by entering the following
cc -o beancount beancount.c
This allows you to use the beancount executable as the interpreter for the financial data in the she-bang:
#!/usr/bin/beancount account Barclays ################################ # ticker shares at stock VOD.L 10 120.17 # ...
If the file with this code, money, is executable, you can simply enter money to launch the money counter. Although it looks like a configuration file, we actually have an executable script. Figure 2 shows you the output. Practical!
The interpreter in Listing 1 is quite sparse: it creates an instance of a Plugger type object, calls init() to initialize the underlying plugin architecture, and passes the configuration file, which it read from standard input using <> previously, to the plugin system’s parse() method. The Plugger framework in Listing 2 interprets the first word in each line as a command. But without plugins, it can’t interpret a single thing. In fact, all it does is ignore the money file’s comment lines, which all start with #.
Plugger.pm automatically parses any modules added to the Plugger/ directory during compilation. Line 7 pulls in the the Module::Pluggable CPAN module, which handles all this. Lines 8 and 9 set the require flag and the search path to the plugins relative to the current directory or @INC path.
Our Plugins do not have a new() constructor, in contrast to the typical object-oriented approach, but an init() function, which is called by Plugger.pm, the master of all plugins, one by one for each plugin module it finds. Module::Pluggable automatically adds the plugins() method to its host, the Plugger class. plugins() returns a list with the names of all discovered plugins. Lines 31 and 32 use this mechanism to iterate through the init() functions of all the plugins.
To allow a plugin to know its caller, and run the caller’s methods if required, Plugger.pm passes a $ctx (context) reference to the init() method of each plugin. This is simply a reference to the only existing Plugger object, the plugin manager. This reference allows a plugin to issue instructions to the Plugger manager. As Plugger interprets the commands in a configuration file, the plugin calls the register_cmd() management method to register new commands.
Listing 3 gives you an example of a plugin in the Plugger/ directory: Account.pm uses the register_cmd() mechanism described earlier in this article to teach the plugin manager the account command:
$ctx->register_cmd("account", \&start, \&process, U \&finish);
Within the confines of the framework, this two-liner specifies that on interpreting the account keyword in the configuration file, Plugger should call the process() function in Plugger/Account.pm and pass it the split elements of the configuration line as arguments. Plugger.pm also calls the start() function shown in Listing 3, line 21, before starting to interpret the configuration file, and finishes off by calling the finish() function (line 84).
The account plugin makes use of this mechanism to set the total value for all defined accounts, which is stored in the global variable account_total, to zero before starting to parse. We still need to decide where to define a counter of this kind, which Account.pm and other plugins will need to access. The Plugger.pm module creates a hash called %MEM for this purposes. The module passes a reference to the %MEM hash to anything that uses the mem() accessor in line 36 to ask for a reference. For example, a plugin such as Account.pm can do the following:
$ctx->mem()->U {account_total} = 0;
to set a variable which other plugins with a reference to the Plugger plugin manager can access, thanks to $ctx. In fact, this technique demonstrates how the Account.pm and Position.pm plugins pass information: Account.pm first sets account_total to zero. Position.pm, which is used by every stock or cash definition, evaluates this and adds it to account_total.
Imagine you want Account.pm to output the top line of an account and the balance in blue and in bold type. The CPAN Term::ANSIColor module handles this nicely. Adding a :constants tag to the use statement exports text attribute constants, such as BLUE, BOLD, and RESET (back to standard type) to the namespace of the calling script. This allows you to issue print statements such as
print BLUE, BOLD, "In blue and bold!",U RESET;
to output ANSI sequences that output the blue and bold text in the current terminal before calling RESET to fall back to normal type for any following print statements.
Listing 3: Account.pm |
001 ############################# 002 package Plugger::Account; 003 ############################# 004 use strict; 005 use warnings; 006 use Term::ANSIColor 007 qw(:constants); 008 009 ############################# 010 sub init { 011 ############################# 012 my ($class, $ctx) = @_; 013 014 $ctx->register_cmd( 015 "account", \&start, 016 \&process, \&finish 017 ); 018 } 019 020 ############################# 021 sub start { 022 ############################# 023 my ($ctx) = @_; 024 025 $ctx->mem() 026 ->{account_total} = 0; 027 } 028 029 ############################# 030 sub account_start { 031 ############################# 032 my ($ctx, $name) = @_; 033 034 print BOLD, BLUE, 035 "Account: $name\n", 036 RESET; 037 038 $ctx->mem() 039 ->{account_subtotal} = 0; 040 $ctx->mem() 041 ->{account_current} = 042 $name; 043 } 044 045 ############################# 046 sub account_end { 047 ############################# 048 my ($ctx, $name) = @_; 049 050 print BOLD, BLUE; 051 printf "%-47s %9.2f\n\n", 052 "Subtotal:", $ctx->mem() 053 ->{account_subtotal}; 054 print RESET; 055 } 056 057 ############################# 058 sub account_end_all { 059 ############################# 060 my ($ctx) = @_; 061 062 print BOLD, BLUE; 063 printf "%-47s %9.2f\n\n", 064 "Total:", $ctx->mem() 065 ->{account_total}; 066 print RESET; 067 } 068 069 ############################# 070 sub process { 071 ############################# 072 my ($ctx, @args) = @_; 073 074 my $c = 075 $ctx->mem() 076 ->{account_current}; 077 account_end($ctx, $c) 078 if $c; 079 account_start($ctx, 080 $args[1]); 081 } 082 083 ############################# 084 sub finish { 085 ############################# 086 my ($ctx) = @_; 087 088 my $c = 089 $ctx->mem() 090 ->{account_current}; 091 account_end($ctx, $c) 092 if $c; 093 account_end_all($ctx); 094 } 095 096 ############################# 097 sub position { 098 ############################# 099 my ( 100 $type, $ticker, 101 $n, $at, 102 $price, $value, 103 $gain 104 ) 105 = @_; 106 107 unless (defined $ticker) { 108 printf "%-47s %9.2f\n", 109 $type, $value; 110 return; 111 } 112 113 my $clr = 114 $gain > 0 ? GREEN: RED; 115 116 printf 117 "%-8s %-10s %9.3f %9.3f" 118 . " %7.2f %9.2f" 119 . " %s(%+9.2f)%s\n", 120 $type, $ticker, $n, $at, 121 $price, $value, $clr, 122 $gain, RESET; 123 } 124 125 1; |
The Position plugin in Listing 4 retrieves current (that is 20 minutes delayed) share prices from the Yahoo financial page using another CPAN module, Finance::YahooQuote. An exported function, getonequote(), gives you ticker symbols such as VOD.L for Vodafone shares on the London stock exchange, or EBAY for the Ebay shareprice on the Nasdaq. There is a useful list of UK ticker symbols at [3].
As beancounter may need the same share price multiple times, Position stores the price in a cache for 10 minutes. The CPAN Cache::Cache module has an extremely simple interface with set(), which sets a cache entry, and get(), which gets the cache entry back later. The implementations include an in-memory cache called Cache:: MemoryCache and Cache::FileCache a file-based persistent cache. Position.pm uses the following
my $cache = Cache::U FileCache->new( { namespace =>U 'Beancount', default_expires_in U => 600, });
to create a cache object and handles all the details, such as efficient storage in temporary files without colliding with other applications. Users simply call $cache->set() and $cache->get().
Listing 4: Position.pm |
001 ############################# 002 package Plugger::Position; 003 ############################# 004 use strict; 005 use warnings; 006 use Log::Log4perl qw(:easy); 007 use Finance::YahooQuote; 008 use Term::ANSIColor; 009 010 ############################# 011 sub init { 012 ############################# 013 my ($class, $ctx) = @_; 014 015 DEBUG "Registering @_"; 016 017 $ctx->register_cmd( 018 "stock", undef, 019 \&process, undef 020 ); 021 $ctx->register_cmd("cash", 022 undef, \&process_cash, 023 undef); 024 } 025 026 ############################# 027 sub process { 028 ############################# 029 my ($ctx, $cmd, @args) = 030 @_; 031 032 my $value = 033 price($args[0]) * 034 $args[1]; 035 my $gain = 036 $value - $args[2] * 037 $args[1]; 038 039 Plugger::Account::position( 040 ucfirst($cmd), 041 @args[ 0 .. 2 ], 042 price($args[0]), 043 $value, 044 $gain 045 ); 046 047 my $mem = $ctx->mem(); 048 $mem->{account_subtotal} += 049 $value; 050 $mem->{account_total} += 051 $value; 052 } 053 054 ############################# 055 sub process_cash { 056 ############################# 057 my ($ctx, $cmd, @args) = 058 @_; 059 060 my $mem = $ctx->mem(); 061 $mem->{account_subtotal} += 062 $args[0]; 063 $mem->{account_total} += 064 $args[0]; 065 066 Plugger::Account::position( 067 ucfirst($cmd), 068 (undef) x 4, 069 $args[0], undef); 070 } 071 072 use Cache::FileCache; 073 074 my $cache = 075 Cache::FileCache->new( 076 { 077 namespace => 'Beancount', 078 default_expires_in => 079 600, 080 } 081 ); 082 083 ############################# 084 sub price { 085 ############################# 086 my ($stock) = @_; 087 088 DEBUG 089 "Fetching $stock quote"; 090 091 my $cached = 092 $cache->get($stock); 093 094 if (defined $cached) { 095 DEBUG "Cached: $cached"; 096 return $cached; 097 } 098 099 my @quote = 100 getonequote $stock; 101 102 die "$stock failed" 103 unless @quote; 104 $cache->set($stock, 105 $quote[2]); 106 107 return $quote[2]; 108 } 109 110 1; |
Of course, beancounter is totally over-engineered for a simple statement of account tool – obviously the work of an architecture astronaut; thanks to Joel Spolsky for hitting the nail on the head at [4]. The plugger framework really comes to its own when it is asked to add user-specific functionality without modifying the original code.
The Plugger/TaxedPosition.pm plugin in Listing 5 gives you an example. Plugger/TaxedPosition.pm subtracts 50 percent tax from (possible) share profits defined by txstock. This “Desert island dreams” mode gives you the bottom line if you were to cash your shares and pay taxes at 50 percent. TaxedPosition does not subtract anything if your shares have made a loss, but simply gives you the face value of the shares after liquidating the losers.
Depending on the scenario, users can write plugins for new keywords, add them to the framework, and modify the system. As TaxedPosition.pm references the price() function defined in Position.pm, it makes sense to use an inheritance or interface mechanism to link TaxedPosition.pm and Position.pm. As the plugger framework does not have classes, TaxedPosition.pm in line 10 of Listing 5 simply defines an AUTOLOAD handler which channels calls from unknown functions to Position.pm.
To facilitate screen output and ensure manageability, the Position.pm plugin handles all screen output. The position() function expects the data for an output item: type, ticker symbol, number, buying price, current price, current total value, profit and loss, and provides neatly formatted output of the results. Cash entries only need the left and right columns.
Your own plugins should utilize Plugger::Account’s position method “printing” just like TaxedPosition.pm. The price() function in Position.pm should also prove useful for your own plugins.
Both the beancounter script (Listing 1) and the compiled C wrapper beancount need to be stored in /usr/bin and must be executable. Plugger.pm (Listing 2) and all the plugins below Plugger/ should be in one of your Perl environment’s @INC paths. If not, you could use a line such as
use lib '/home/mschilli/U perl-modules';
in the beancounter Perl script to publish the path, assuming that Plugger & co. are stored in the directory specified here. The Module::Pluggable, Finance::YahooQuote and Term::ANSIColor modules are available from CPAN. The easiest way to install them is to use a CPAN shell. After doing so, there is nothing to stop Perl from ruling your finances!
Listing 5: TaxedPosition.pm |
01 ############################# 02 package 03 Plugger::TaxedPosition; 04 ############################# 05 use strict; 06 use warnings; 07 use Log::Log4perl qw(:easy); 08 09 ############################# 10 sub AUTOLOAD { 11 ############################# 12 no strict qw(vars refs); 13 14 (my $func = $AUTOLOAD) =~ 15 s/.*::/Plugger::Position::/; 16 $func->(@_); 17 } 18 19 ############################# 20 sub init { 21 ############################# 22 my ($class, $ctx) = @_; 23 24 $ctx->register_cmd( 25 "txstock", undef, 26 \&process, undef 27 ); 28 } 29 30 ############################# 31 sub process { 32 ############################# 33 my ($ctx, $cmd, @args) = 34 @_; 35 36 my $value = 37 price($args[0]) * 38 $args[1]; 39 my $gain = 40 $value - $args[2] * 41 $args[1]; 42 43 my $tax = $gain / 2; 44 45 $value -= $tax 46 if $gain > 0; 47 $gain -= $tax if $gain > 0; 48 49 Plugger::Account::position( 50 ucfirst($cmd), 51 @args[ 0 .. 2 ], 52 price($args[0]), 53 $value, 54 $gain 55 ); 56 57 my $mem = $ctx->mem(); 58 $mem->{account_subtotal} += 59 $value; 60 $mem->{account_total} += 61 $value; 62 } 63 64 1; |
INFO |
[1] Listings: http://www.linux-magazine.com/Magazine/Downloads/53/Perl [2] Module::Pluggable tutorial: http://www.perladvent.org/2004/6th [3] Symbols for popular UK shares: http://uk.biz.yahoo.com/p/uk/cpi/cpia0.html [4] Joel Spolsky, “Don’t let Architecture Astronauts scare you” in Joel on Software: Apress 2004. |