#!/usr/bin/perl -w
###########################################
# pofo - draw a stacked portfolio graph
# Mike Schilli, 2007 (
[email protected])
###########################################
use strict;
use CachedQuote;
use DateTime;
use RRDTool::OO;
use Log::Log4perl qw(:easy);
#Log::Log4perl->easy_init($DEBUG);
my @colors = qw(f35b78 e80707 7607e8
0a5316 073f6f 59b0fb);
my $cq = CachedQuote->new();
my($cfg_file) = @ARGV;
die "usage: $0 cfgfile" unless $cfg_file;
my @symbols;
my $acts = cfg_read($cfg_file, \@symbols);
my %pos = ();
my $end = DateTime->today();
my $start = $end->clone->subtract(
years => 1);
for my $act (sort keys %$acts) {
next if $acts->{$act}->[0]->[0]
>= $start;
pos_add(\%pos, $_) for @{$acts->{$act}};
}
my $counter = 0;
my %symbol_colors;
for (@symbols) {
my $idx = ($counter++ % @colors);
$symbol_colors{$_} = $colors[$idx];
}
unlink my $rrdfile = "holdings.rrd";
my $rrd = RRDTool::OO->new(
file => $rrdfile,
);
$rrd->create(
step => 24*3600,
start => $start->epoch() - 1,
map({
( data_source => {
name => tick_clean($_),
type => "GAUGE",
},
)} @symbols),
archive => { rows => 5000,
cfunc => "MAX" }
);
for(my $dt = $start->clone;
$dt <= $end;
$dt->add( days => 1)) {
if(exists $acts->{$dt}) {
pos_add(\%pos, $_) for @{$acts->{$dt}};
}
my %parts = ();
my $total = sum_up(\%pos, $dt, \%parts);
INFO "*** TOTAL *** = $total\n";
$rrd->update(
time => $dt->epoch(),
values => \%parts,
) if scalar keys %parts;
}
$rrd->graph(
width => 800,
height => 600,
lower_limit => 0,
image => "positions.png",
vertical_label => "Positions",
start => $start->epoch(),
end => $end->epoch(),
map { ( draw => {
type => "stack",
dsname => tick_clean($_),
color => $symbol_colors{$_},
legend => $_,
} )
} @symbols,
);
###########################################
sub sum_up {
###########################################
my($all, $dt, $parts) = @_;
my $sum = 0;
for my $tick (keys %$all) {
my $q = 1;
$q = $cq->quote($dt, $tick) if
$tick ne 'cash';
my $add = $all->{$tick} * $q;
$parts->{tick_clean($tick)} = $add;
$sum += $add;
DEBUG "Add: $all->{$tick} $tick $add";
}
return $sum;
}
###########################################
sub pos_add {
###########################################
my($all, $pos) = @_;
my($dt, $act, $tick, $n) = @{ $pos };
die "pos: @$pos" if ! defined $n;
DEBUG "Action: $act $n $tick";
my $q = 1;
$q = $cq->quote($dt, $tick) if
$tick ne 'cash';
my $val = $n * $q;
if($tick eq "cash") {
$all->{cash} += $val if $act eq "in";
$all->{cash} -= $val if $act eq "out";
$all->{cash} = $val if $act eq "chk";
} else {
if($act eq "in") {
$all->{$tick} += $n;
$all->{cash} -= $val;
} elsif($act eq "out") {
$all->{$tick} -= $n;
$all->{cash} += $val;
} elsif($act eq "find") {
$all->{$tick} += $n;
}
DEBUG "After: $tick: $all->{$tick}";
}
$all->{cash} ||= 0;
DEBUG "After: Cash: $all->{cash}";
}
###########################################
sub cfg_read {
###########################################
my($cfgfile, $symbols) = @_;
my %by_date = ();
open FILE, "<$cfgfile" or
die "Cannot open $cfgfile ($!)";
while(<FILE>) {
chomp;
s/#.*//;
my @fields = split ' ', $_;
next unless @fields; # empty line
my $dt = dt_parse( $fields[0] );
$fields[0] = $dt;
push @$symbols, $fields[2] unless
grep { $_ eq $fields[2] } @$symbols;
push @{ $by_date{ $dt } }, [ @fields ];
}
close FILE;
return \%by_date;
}
###########################################
sub dt_parse {
###########################################
my($string) = @_;
my $fmt = DateTime::Format::Strptime->
new( pattern => "%Y-%m-%d" );
return $fmt->parse_datetime($string);
}
###########################################
sub tick_clean {
###########################################
my($tick) = @_;
$tick =~ s/./_/g;
return $tick;
}