The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Copyright 2007, 2008, 2009, 2010 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 <http://www.gnu.org/licenses/>.

package App::Chart::Series::Derived::Volume;
use 5.008;
use strict;
use warnings;
use Carp;
use List::Util qw(min max);
use Locale::TextDomain ('App-Chart');
use base 'App::Chart::Series::Indicator';

use constant DEBUG => 0;

sub longname  { __('Volume') }
*shortname = \&longname;
# FIXME
# sub manual    { __p('manual-node','Volume and Open Interest') }

use constant
  { type       => 'indicator',
    units      => 'volume',
    priority   => 10,
    minimum    => 0,
    decimals   => 0,  # no decimals normally
    default_linestyle => 'Bars',
  };

sub new {
  my ($class, $parent) = @_;

  my $p = $parent->array('volumes')
    || croak "No volumes in series '",$parent->name,"'";
  return $class->SUPER::new (parent => $parent,
                             arrays => { values => $p });
}

sub fill_part {
  my ($self, $lo, $hi) = @_;
  my $parent = $self->{'parent'};
  $parent->fill_part ($lo, $hi);
}

# Return (LOWER UPPER) which is a suggested initial Y-axis page range to
# show for dates LO to HI.  This is for use with trading volumes and also
# open-interest.
#
# The page size is meant to keep average to high volume days visible in the
# window, but leave abnormally high days off the top of the screen so as not
# to make the average ones come out tiny.
#
# The rule is to look for the 98% fractile, meaning a level which covers 98%
# of the values in the LO to HI range, then an extra 1.5x that much.  Making
# UPPER just the 0.98 fractile alone would guarantee two or three points off
# the top of the window even if they're only modestly bigger than the
# average, hence going up to 1.5x that point.
#
#
# ENHANCE-ME: If LO to HI is small then maybe the 98% should be reduced a
# bit.  If the width is less than 50 (or is it 25?) dates then 98% ends up
# meaning every data point.
#
sub initial_range {
  my ($self, $lo, $hi) = @_;
  if (DEBUG) { print "Volume initial range\n"; }
  $lo = max ($lo, 0);
  $hi = max ($hi, 0);

  $self->fill ($lo, $hi);
  my $values = $self->values_array;
  my @v = grep{defined} @{$values}[$lo..$hi];

  if (defined (my $symbol = $self->symbol)) {
    my $latest = App::Chart::Latest->get($symbol);
    my $timebase = $self->timebase;
    if (defined $latest->{'volume'}
        && defined (my $last_iso = $latest->{'last_date'})) {
      my $last_t = $timebase->from_iso_floor ($last_iso);
      if ($last_t >= $lo && $last_t <= $hi) {
        push @v, $latest->{'volume'};
      }
    }
  }

  if (! @v) {
    return; # no non-undef values at all
  }

  # ENHANCE-ME: the top few values can probably be found without a full sort
  my $lastval = $v[-1];
  @v = sort {$a <=> $b} @v;

  return (0, max (0,
                  $lastval,        # always fit last value
                  $v[$#v * 0.98],  # fractile
                  1.5 * List::Util::sum (@v) / scalar @v)); # 1.5*mean
}

1;
__END__

# =head1 NAME
# 
# App::Chart::Series::Derived::Volume -- trading volume series
# 
# =head1 SYNOPSIS
# 
#  my $vol = $series->Volume;
# 
# =head1 CLASS HIERARCHY
# 
#     App::Chart::Series
#       App::Chart::Series::Derived::Volume
# 
# =head1 DESCRIPTION
# 
# A C<App::Chart::Series::Derived::Volume> series picks out just the volumes
# from a series.  Usually this will be a database series
# (C<App::Chart::Series::Database>), but anything with a C<volumes> array can
# be used.  A Volumes series allows those volume values to be passed into
# other series calculations or displays.
# 
# =head1 SEE ALSO
# 
# L<App::Chart::Series>, L<App::Chart::Series::Database>
# 
# =cut