# Copyright 2007, 2008, 2009, 2010, 2011 Kevin Ryde # This file is part of Chart. # # Chart 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 3, or (at your option) any later version. # # Chart 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. # # You should have received a copy of the GNU General Public License along # with Chart. If not, see . package App::Chart::Series::Database; use 5.010; use strict; use warnings; use Carp; use Scalar::Util; use App::Chart::Database; use App::Chart::DBI; use base 'App::Chart::Series::OHLCVI'; our $VERSION = 246; use constant DEBUG => 0; # %cache is keyed by symbol string, with value a App::Chart::Series::Database # object. The value reference is weakened so it becomes undef when # otherwise unused. # our %cache = (); sub _purge_cache_on_data_changed { my ($symbol_hash) = @_; if (DEBUG) { print "data-changed, purge series: ", join (', ', grep {exists $cache{$_}} keys %$symbol_hash),"\n"; } delete @cache{keys %$symbol_hash}; # hash slice } use constant::defer _init_cache => sub { App::Chart::chart_dirbroadcast()->connect_first ('data-changed', \&_purge_cache_on_data_changed); return; }; sub new { my ($class, $symbol) = @_; if (DEBUG) { print "Series new $symbol\n"; } my $self = $cache{$symbol}; if ($self) { return $self; } my $base = App::Chart::DBI->read_single ('SELECT date FROM daily WHERE symbol=? ORDER BY date ASC LIMIT 1', $symbol); if (! $base) { require App::Chart::TZ; my $timezone = App::Chart::TZ->for_symbol ($symbol); $base = $timezone->iso_date; } if (DEBUG) { print " base $base\n"; } require App::Chart::Timebase::Days; my $timebase = App::Chart::Timebase::Days->new_from_iso ($base); $self = $class->SUPER::new (symbol => $symbol, timebase => $timebase); # lose any cache entries which have gone undef through weaks destroyed delete @cache{grep {! $cache{$_}} keys %cache}; # add new entry _init_cache(); $cache{$symbol} = $self; Scalar::Util::weaken ($cache{$symbol}); return $self; } sub hi { my ($self) = @_; if (! exists $self->{'hi'}) { if (DEBUG) { print "Series hi for $self->{'symbol'}\n"; } my $date = App::Chart::DBI->read_single ('SELECT date FROM daily WHERE symbol=? ORDER BY date DESC LIMIT 1', $self->{'symbol'}); if (DEBUG) { print " iso ",$date//'undef',"\n"; } my $timebase = $self->{'timebase'}; $self->{'hi'} = ($date ? $timebase->from_iso_floor ($date) : 0); if (DEBUG) { print " hi=$self->{'hi'}\n"; } } return $self->{'hi'}; } sub fill_part { my ($self, $lo, $hi) = @_; if (DEBUG) { print "Database $self->{'symbol'} fill_part $lo $hi\n"; } my $dbh = App::Chart::DBI->instance; my $timebase = $self->{'timebase'}; # date descending so first store pre-extends the respective arrays my $sth = $dbh->prepare_cached ('SELECT date, open, high, low, close, volume, openint FROM daily WHERE (symbol=? AND (date BETWEEN ? AND ?)) ORDER BY date DESC'); my $aref = $dbh->selectall_arrayref ($sth, undef, $self->{'symbol'}, $timebase->to_iso ($lo), $timebase->to_iso ($hi)); $sth->finish; my $opens = $self->array('opens'); my $highs = $self->array('highs'); my $lows = $self->array('lows'); my $closes = $self->array('closes'); my $volumes = $self->array('volumes'); my $openints = $self->array('openints'); foreach my $row (@$aref) { my $i = $timebase->from_iso_floor ($row->[0]); next if ($i < 0); if (defined $row->[1]) { $opens->[$i] = $row->[1]; } if (defined $row->[2]) { $highs->[$i] = $row->[2]; } if (defined $row->[3]) { $lows->[$i] = $row->[3]; } if (defined $row->[4]) { $closes->[$i] = $row->[4]; } if (defined $row->[5]) { $volumes->[$i] = $row->[5]; } if (defined $row->[6]) { $openints->[$i] = $row->[6]; } } } sub name { my ($self) = @_; return $self->symbol; # in Series.pm } # { # $self->{'symbol'} # # if (! exists $self->{'name'}) { # $self->{'name'} = App::Chart::Database->symbol_name ($self->{'symbol'}); # } # return $self->{'name'} # } sub symbol_name { my ($self) = @_; return App::Chart::Database->symbol_name ($self->{'symbol'}); } sub decimals { my ($self) = @_; if (! exists $self->{'decimals'}) { $self->{'decimals'} = App::Chart::Database->symbol_decimals ($self->{'symbol'}); } return $self->{'decimals'}; } sub dividends { my ($self) = @_; return ($self->{'dividends'} ||= do { my $dbh = App::Chart::DBI->instance; my $sth = $dbh->prepare_cached ('SELECT ex_date, type, amount, imputation, qualifier, note FROM dividend WHERE symbol=? ORDER BY ex_date ASC'); my $aref = $dbh->selectall_arrayref ($sth, {Slice=>{}}, $self->{'symbol'}); my $timebase = $self->{'timebase'}; foreach my $div (@$aref) { foreach my $date (qw(ex_date record_date pay_date)) { my $iso = $div->{$date} or next; my $t = $timebase->from_iso_floor ($iso); $div->{$date.'_t'} = $t; } } $aref }); } sub splits { my ($self) = @_; return ($self->{'splits'} ||= do { my $dbh = App::Chart::DBI->instance; my $sth = $dbh->prepare_cached ('SELECT date, new, old, note FROM split WHERE symbol=? ORDER BY date ASC'); my $aref = $dbh->selectall_arrayref ($sth, {Slice=>{}}, $self->{'symbol'}); my $timebase = $self->{'timebase'}; foreach my $div (@$aref) { my $iso = $div->{'date'} or next; my $t = $timebase->from_iso_floor ($iso); $div->{'date_t'} = $t; } $aref }); } sub annotations { my ($self) = @_; return ($self->{'annotations'} ||= do { if (DEBUG) { print "Series::Database read annotations ", $self->{'symbol'},"\n"; } my $dbh = App::Chart::DBI->instance; my $sth = $dbh->prepare_cached ('SELECT id, date, note FROM annotation WHERE symbol=? ORDER BY date ASC'); my $aref = $dbh->selectall_arrayref ($sth, {Slice=>{}}, $self->{'symbol'}); my $timebase = $self->{'timebase'}; foreach my $ann (@$aref) { my $iso = $ann->{'date'} or next; my $t = $timebase->from_iso_floor ($iso); $ann->{'date_t'} = $t; } $aref }); } sub Alerts_arrayref { my ($series) = @_; return ($series->{__PACKAGE__.'.array'} ||= do { my $symbol = $series->symbol || ''; require App::Chart::DBI; my $dbh = App::Chart::DBI->instance; my $sth = $dbh->prepare_cached ('SELECT * FROM alert WHERE symbol=?'); my $aref = $dbh->selectall_arrayref ($sth, {Slice=>{}}, $symbol); $sth->finish; if (@$aref) { require App::Chart::Annotation; foreach my $elem (@$aref) { bless $elem, 'App::Chart::Annotation::Alert'; } } $aref; }); } 1; __END__ =for stopwords OHLCVI =head1 NAME App::Chart::Series::Database -- symbol data series from database =head1 SYNOPSIS use App::Chart::Series::Database; my $series = App::Chart::Series::Database->new ('BHP.AX'); =head1 CLASS HIERARCHY App::Chart::Series App::Chart::Series::OHLCVI App::Chart::Series::Database =head1 FUNCTIONS =over 4 =item C<< $series = App::Chart::Series::Database->new ($symbol) >> Return a series object which is the OHLCVI data for C<$symbol> from the database. If the database changes then C should be called again to get a new object with new values, date range, etc. =back =head1 SEE ALSO L =head1 HOME PAGE L =head1 LICENCE Copyright 2008, 2009, 2010, 2011 Kevin Ryde Chart 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 3, or (at your option) any later version. Chart 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. You should have received a copy of the GNU General Public License along with Chart; see the file F. Failing that, see L. =cut