The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Copyright 2007, 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::LaguerreFilter;
use 5.010;
use strict;
use warnings;
use Carp;
use List::Util qw(min max);
use Locale::TextDomain ('App-Chart');

use base 'App::Chart::Series::Indicator';
use App::Chart::Series::Calculation;
use App::Chart::Series::Derived::EMA;
use App::Chart::Series::Derived::EMAx2;

# http://www.mesasoftware.com/technicalpapers.htm
# http://www.mesasoftware.com/Papers/TIME%20WARP.pdf
#     Paper by John Elhers.
#
# http://www.mesasoftware.com/seminars.htm
# http://www.mesasoftware.com/Seminars/TradeStation%20World%2005.pdf
# http://www.mesasoftware.com/Seminars/Seminars/TSWorld05.ppt
#     (View the powerpoint with google.)
#     Summary by John Ehlers of several of his and other averages.
#     * A Laguerre filter warps time in the filter coefficients
#       - Enables extreme smoothing with just a few filter terms
#     * A NonLinear Laguerre filter measures the difference between the
#       current price and the last computed filter output.
#       - Objective is to drive this "error" to zero
#       - The "error", normalized to the error range over a selected period
#         is the alpha of the Laguerre filter
#


sub longname   { __('Laguerre Filter') }
sub shortname  { __('Laguerre') }
sub manual     { __p('manual-node','Laguerre Filter') }

use constant
  { type       => 'average',
    parameter_info => [ { name     => __('Alpha'),
                          key      => 'laguerre_filter_alpha',
                          type     => 'float',
                          minimum  => 0,
                          maximum  => 1,
                          default  => 0.2,
                          decimals => 2,
                          step     => 0.1 } ],
  };

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

  $alpha //= parameter_info()->[0]->{'default'};
  ($alpha >= 0 && $alpha <= 1.0) || croak "Laguerre Filter bad alpha: $alpha";

  return $class->SUPER::new
    (parent     => $parent,
     parameters => [ $alpha ],
     arrays     => { values => [] },
     array_aliases => { });
}

sub proc {
  my ($class, $alpha) = @_;
  $alpha = max (0.00001, min (0.99999, $alpha));
  my $proc_for_alpha = $class->proc_for_alpha();
  return sub {
    my ($value) = @_;
    return $proc_for_alpha->($value, $alpha);
  };
}

sub proc_for_alpha {
  my ($class) = @_;

  my $a_sum = 0;
  my $a_div = 0;
  my ($a_prev, $a_pdiv);

  my $b_sum = 0;
  my $b_div = 0;
  my ($b_prev, $b_pdiv);

  my $c_sum = 0;
  my $c_div = 0;
  my ($c_prev, $c_pdiv);

  my $d_sum = 0;
  my $d_div = 0;

  return sub {
    my ($value, $alpha) = @_;
    my $f = 1 - $alpha;

    $a_prev = $a_sum;
    $a_pdiv = $a_div;
    $a_sum = $value * $alpha + $a_sum * $f;
    $a_div =          $alpha + $a_div * $f;

    $b_prev = $b_sum;
    $b_pdiv = $b_div;
    $b_sum = $a_prev + ($b_sum - $a_sum) * $f;
    $b_div = $a_pdiv + ($b_div - $a_div) * $f;

    $c_prev = $c_sum;
    $c_pdiv = $c_div;
    $c_sum = $b_prev + ($c_sum - $b_sum) * $f;
    $c_div = $b_pdiv + ($c_div - $b_div) * $f;

    $d_sum = $c_prev + ($d_sum - $c_sum) * $f;
    $d_div = $c_pdiv + ($d_div - $c_div) * $f;

    return ($a_sum/$a_div
            + 2 * $b_sum/$b_div
            + 2 * $c_sum/$c_div
            + $d_sum/$d_div) / 6.0;
  };
}

# warmup_count() gives a fixed amount, based on the worst-case EMA alphas
# all the slowest possible.  It ends up being 1656 which is hugely more than
# needed in practice.
#
# warmup_count_for_position() calculates a value on actual data, working
# backwards.  In practice it's as little as about 100.
#
sub warmup_count {
  my ($self_or_class, $alpha) = @_;

  $alpha = max (0.00001, min (0.99999, $alpha));
  my $f = 1 - $alpha;

  return App::Chart::Series::Derived::EMAx2::bsearch_first_true
    (sub {
       my ($i) = @_;
       return (laguerre_omitted($f,$i)
               <= App::Chart::Series::Derived::EMA::WARMUP_OMITTED_FRACTION);
     },
     App::Chart::Series::Derived::EMA::alpha_to_N($alpha));
}

# see devel/ema-omitted.pl
sub laguerre_omitted {
  my ($f, $k) = @_;
  return
$f ** ($k-2)
  * ((1/36*$k**2 + -1/36)*$k
    + ($f      # f^($k-1)
      * ((1/4*$k + 1/4)*$k
        + ($f      # f^$k
          * ((((-1/12*$k + -1/6)*$k + 3/4)*$k + 5/6)
            + ($f      # f^($k+1)
              * (((-1/2*$k + -1)*$k + 1/2)
                + ($f      # f^($k+2)
                  * ((((1/12*$k + 1/3)*$k + -5/12)*$k + -2/3)
                    + ($f      # f^($k+3)
                      * (((1/4*$k + 3/4)*$k + 1/2)
                        + ($f      # f^($k+4)
                          * ((((-1/36*$k + -1/6)*$k + -11/36)*$k + -1/6)
                            )))))))))))));
}

1;
__END__

# =head1 NAME
# 
# App::Chart::Series::Derived::LaguerreFilter -- Laguerre Filter moving average
# 
# =head1 SYNOPSIS
# 
#  my $series = $parent->LaguerreFilter($alpha);
# 
# =head1 DESCRIPTION
# 
# ...
# 
# =head1 SEE ALSO
# 
# L<App::Chart::Series>, L<App::Chart::Series::Derived::EMA>
# 
# =cut