The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w

# 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/>.


use 5.010;
use strict;
use warnings;
use Memoize;
use List::Util qw(min max);
use List::MoreUtils;
use Math::BigRat try => 'GMP';
use Math::Polynomial 1.002; # incompatible with 0.04 ...
use Math::Polynomial::Horner;

# uncomment this to run the ### lines
#use Smart::Comments;

$| = 1;
Math::Polynomial->string_config ({variable      => '$x',
                                 times          => '*',
                                 power          => '**',
                                  leading_minus => '-', # no space
                                 fold_one       => 1,
                                 fold_sign      => 1,
                                 fold_sign_swap_end => 1,
                                 power_by_times     => 1,
                                 });


#------------------------------------------------------------------------------

sub Math::Polynomial::mul1c {
  my ($self, $c) = @_;
  #   print "mul1c $self  by  ",Math::Polynomial->new(-$c,1),"\n";
  #   print "  got $self\n";
  return $self * Math::Polynomial->new (-$c, 1);
}
sub Math::Polynomial::div1c {
  my ($self, $c) = @_;
  return $self / Math::Polynomial->new (-$c, 1);
}


#------------------------------------------------------------------------------

sub print_ma_terms {
  my ($name) = @_;
  print "$name\n";
  my $term_proc = do { no strict; \&$name };
  foreach my $k (0 .. 20) {
    say "  p[$k] * (@{[$term_proc->($k)]})";
  }
}

my $poly_0 = Math::Polynomial->new;

# per Knuth vol 2 sect 4.3.3
sub interpolate_successive {
  my $start = 0;
  if (! ref $_[0] && $_[0] eq 'start') {
    shift;
    $start = shift;
  }

  my @data = @_;
  my @divisor;
  my $divisor = 1;
  foreach my $i (1 .. $#data) {
    push @divisor, ($divisor *= $i);
    for (my $j = $#data; $j >= $i; $j--) {
      $data[$j] -= $data[$j-1];
    }
    ### data: \@data
  }
  ### divisor: \@divisor

  my $ret = Math::Polynomial->new((pop @data) / (pop @divisor));
  while (@data > 2) {
    $ret *= Math::Polynomial->new (-($#data + $start), 1);
    $ret += (pop @data) / (pop @divisor);
  }
  # $data[1]
  $ret = $ret->mul_root ($start + 1);
  # $ret *= Math::Polynomial->new (-(1 + $start), 1);
  $ret += (pop @data);

  if ($start) {
    # $ret *= Math::Polynomial->new (-$start, 1);
    $ret = $ret->mul_root ($start);
  }
  $ret <<= 1;
  $ret += (pop @data);
  # push @$ret, pop @data;

  return $ret;
}

# sub interpolate_faster {
#   my (@x, @y);
#   while (@_) {
#     push @x, shift;
#     push @y, shift;
#   }
#   my @nums;
#   my $numerator = Math::Polynomial->new(1);
#   for (my $i = $#x; $i > 0; $i--) {
#     $numerator->mul1c($x[$i]);
#     push @nums, $numerator->clone;
#   }
# 
#   my $result = Math::Polynomial->new(0);
#   foreach my $i (0 .. $#x-1) {
#     $numerator = shift @nums;
#         print "num ",$numerator,"\n";
#   my $constant = pop(@y) / $numerator->eval($_);
#     $result += $numerator * $constant;
#   }
#   return $result;
# }


{
  say Math::Polynomial->new(1)->mul_root(1);
  say Math::Polynomial->new(-1,1)->div_root(1);
  say Math::Polynomial->new(1,1)->mul_root(1);

  say interpolate_successive(10, 304, 1980, 7084, 18526);
  say interpolate_successive(0,1,4);
  say interpolate_successive(1,2,3);
  say interpolate_successive(1, 4, 9, 16);

  my $triangle = sub {
    my ($n) = @_;
    return List::Util::sum (1 .. $n);
  };
  my $sum_squares = sub {
    my ($n) = @_;
    return List::Util::sum (map {$_**2} (1 .. $n));
  };
  my $yden = sub {
    my ($n) = @_;
    return $n * $sum_squares->($n) - ($triangle->($n))**2;
  };
  require Math::Polynomial;
  require Math::BigRat;
  # Math::Polynomial->configure(VARIABLE => '$N');
  my $poly = Math::Polynomial->interpolate
    ([ 1 .. 10 ],
     [ map {Math::BigRat->new(12 * $yden->($_))} (1..10)]);
  #   my $poly = Math::Polynomial::interpolate (map {
  #     ($_, Math::BigRat->new(12 * $yden->($_)))} (1..10));
  say "yfactor 1/12 * ($poly)";

  $poly = Math::Polynomial->interpolate
    ([ 1 .. 10],
     [ map {1*2*3*4*5*6*7*12 * $yden->($_)} (1..10)]);
  say "yfactor 1/12 * ($poly)";

  #   print "\n\n";
  #   $poly = interpolate_faster (map {
  #     ($_, Math::BigRat->new(12 * $yden->($_)))} (1..10));
  #   say "yfactor 1/12 * ($poly)";

  #   print "\n\n";
  #   $poly = interpolate_successive (map {12 * $yden->($_)} (0..10));
  #   say "yfactor 1/12 * ($poly)";

  #  exit 0;
}

sub high_power {
  my ($data) = @_;
  my $power = 0;
  my $deriv = 1;
  for (;;) {
    say "$power $deriv ",@$data;
    if (List::MoreUtils::all {$_ == $data->[0]} @$data) {
      return ($data->[0] / $deriv, $power);
    }
    $data = [ map {$data->[$_] - $data->[$_-1]} (1 .. $#$data) ];
    $power++;
    $deriv *= ($power + 1);
  }
}


#------------------------------------------------------------------------------
print "EMA\n";

# ema_term() returns a polynomial in f=1-alpha which is the coefficient of
# the f^k term.  This is simply (1-f)*f^k = 1*f^k + -1*f^(k+1).
sub ema_term {
  my ($k) = @_;
  #print "ema_term $k\n";

  if ($k < 0) {
    return $poly_0;
  } else {
    # Math::BigRat->new(-1),
    #                                   Math::BigRat->new(1),
    return Math::Polynomial->new ((0) x $k, 1, -1);
  }
}
memoize 'ema_term';

print_ma_terms ("ema_term");
ema_term(0)->is_equal (Math::Polynomial->new(1,-1)) or die;
ema_term(1) == Math::Polynomial->new(0,1,-1) or die;

# return a procedure $weight_proc which called $weight_proc->($k) calculates
# the total weight of the terms up to and including the p[k] input price
# term.
#
# $term_proc->($k) should return a polynomial in f which is the coefficient
# of the f^k term.  The created $weight_proc function simply adds them up.
#
sub make_weight_proc {
  my ($term_proc) = @_;
  my $weight_proc;
  $weight_proc = memoize
    (sub {
       my ($k) = @_;
       # say "weight_to_proc $k";
       return $term_proc->($k) + ($k > 0 ? $weight_proc->($k-1) : $poly_0);
     });
  return $weight_proc;
}

# ema_weight_to($k) returns a polynomial in f which is the total weight of
# terms up to and including the p[k] input.
*ema_weight_to = make_weight_proc(\&ema_term);
say "ema_weight_to(4) ", ema_weight_to(4);

# ema_omitted_after($k) returns a polynomial in f which is the total weight
# omitted by stopping at (and including) the p[k] input.
sub ema_omitted_after {
  my ($k) = @_;
  return 1 - ema_weight_to($k);
}
say "ema_omitted_after(4)  ",ema_omitted_after(4);


#------------------------------------------------------------------------------
print "\n\nEMA x 2\n";

use vars qw($a $b);
sub sum {
  return List::Util::reduce {$a+$b} @_;
}

# $term_proc->($i) should return a polynomial in f which is the weight of the
# p[i] input term in some weighted mean series.
# ema_of_term_proc() returns a new procedure $smoothed_proc which called
# $smoothed_proc->($i) similarly returns a polynomial in f, but representing
# the result of smoothing $term_proc with an EMA.
#
sub ema_of_term_proc {
  my ($term_proc) = @_;
  return memoize
    (sub {
       my ($k) = @_;
       # print "ema_of_term $k\n";
       my $_1_f = Math::Polynomial->new (1, -1); # 1-f

       sum (map {
         # ema_term($_) * $term_proc->($k-$_)
         #          say "T=", $term_proc->($k-$_);
         #          say "#=", ema_term($_);
         #          say "P ", ema_term($_) * $term_proc->($k-$_);

         my $i = $_;
         my $prod = $term_proc->($k-$i);
         # say "N ", $prod;
         $prod <<= $i;    # *f^e
         # say "NP ", $prod;
         $prod *= $_1_f;  # *(1-f)*f^e
         # say "X ", $prod;

         $prod
       } (0 .. $k));
     });
}

# ema2_term() returns a polynomial in f=1-alpha which is the coefficient of
# the f^k term.  This is simply (1-f)*f^k = 1*f^k + -1*f^(k+1).
*ema2_term = ema_of_term_proc(\&ema_term);

print_ma_terms ("ema2_term");
# ema2_term(0) == Math::Polynomial->new(-1,1) or die;
# ema2_term(1) == Math::Polynomial->new(-1,1,0) or die;

*ema2_weight_to = make_weight_proc(\&ema2_term);
say "ema2_weight_to(4) ", ema2_weight_to(4);
sub ema2_omitted_after {
  my ($k) = @_;
  return 1 - ema2_weight_to($k);
}
memoize 'ema2_omitted_after';
say "ema2_omitted_after(4)  ",ema2_omitted_after(4);

# `omitted-fk' returns the coefficient (a number) of the f^(k+offset) term
# in the p[k] omitted polynomial of OMITTED-POLY-PROC.  This is meant for
# finding closed-form expressions (a poly in k) for that f^(k+offset)
# coefficient.
#
# OMITTED-POLY-PROC returns a polynomial in f which is the weight omitted
# from some weighted average by stopping at (and including) the p[k] term.
#
sub omitted_fk {
  my ($omitted_proc, $offset) = @_;
  return memoize (sub {
                    my ($k) = @_;
                    # say "omitted_fk $omitted_proc $offset $k";
                    my $poly = $omitted_proc->($k);
                    my $i = $k+$offset;
                    if ($i > $poly->degree) {
                      return 0;
                    } else {
                      return $poly->coeff($i);
                    }
                  });
}
memoize('omitted_fk');

sub Xclosed_form {
  my ($proc) = @_;
  say "closed_form $proc";
  my @data;
  my @x = (20 .. 25);
  foreach my $i (0 .. $#x) {
    $data[$i] = $proc->($x[$i]);
  }
}


sub closed_form {
  my ($proc) = @_;

  #   my @data = map {
  #     ($_, Math::BigRat->new($proc->($_)))
  #   } (20 .. 30);
  #   print "  interpolate_successive ...\n";
  #   return interpolate_successive (start => 20, @data);

  my @xs = (20 .. 30);
  return Math::Polynomial->interpolate
    (\@xs,
     [ map {
       # say $_;
       # say $proc->($_);
       Math::BigRat->new($proc->($_)) } @xs]);
}
memoize 'closed_form';

sub omitted_fk_closed_form {
  my ($name, $offset) = @_;
  my $proc = do { no strict; \&$name };
  my $poly = closed_form(omitted_fk($proc, $offset));
  say "$name k+$offset  ", $poly;
}

sub omitted_expr {
  my ($name) = @_;
  print "omitted_expr $name ...\n";
  my $proc = do { no strict; \&$name };
  my %a;
  foreach my $offset (-8 .. 20) {
    my $poly = closed_form(omitted_fk($proc, $offset));
    $a{$offset} = $poly;
    if ($poly != 0) {
      printf "f^(k%+d) * (%s)\n", $offset, "$poly";
    }
  }

  while (my ($key, $value) = each %a) {
    if ($value == 0) { delete $a{$key}; }
  }
  if (! %a) {
    die "Oops, omitted_expr all zeros";
  }
  my $start = min(keys %a);
  my $end   = max(keys %a);
  my $level = 1;
  say "\$f ** ",offset_k_form($start);
  foreach my $offset ($start .. $end) {
    if ($offset != $start) {
      say '  'x$level, "+ (\$f      # f^",offset_k_form($offset);
      $level++;
    }
    my $poly = $a{$offset} // $poly_0;
    say '  'x$level, "* (", Math::Polynomial::Horner::as_string($poly);
    $level++;
  }
  say '  'x$level,")"x$level;
}
sub offset_k_form {
  my ($offset) = @_;
  if ($offset == 0) {
    return '$k';
  } else {
    return sprintf "(\$k%+d)", $offset;
  }
}

# 	    (begin
# 	      (format #t "~a  (* f   
# 		      (make-string level #\space)
# 		      offset)
# 	      (set! level (1+ level))))
# 	(set! level (1+ level)))

omitted_fk_closed_form('ema2_omitted_after', 0);
omitted_fk_closed_form('ema2_omitted_after', 1);
omitted_fk_closed_form('ema2_omitted_after', 2);
omitted_fk_closed_form('ema2_omitted_after', 3);
omitted_expr('ema2_omitted_after');

#------------------------------------------------------------------------------
print "\n\nEMA x 3\n";

# ema3_term() returns a polynomial in f=1-alpha which is the coefficient of
# the f^k term.  This is simply (1-f)*f^k = 1*f^k + -1*f^(k+1).
*ema3_term = ema_of_term_proc(\&ema2_term);

print_ma_terms ("ema3_term");

*ema3_weight_to = make_weight_proc(\&ema3_term);
say "ema3_weight_to(4) ", ema3_weight_to(4);

sub ema3_omitted_after {
  my ($k) = @_;
  return 1 - ema3_weight_to($k);
}
memoize 'ema3_omitted_after';
say "ema3_omitted_after(4)  ",ema3_omitted_after(4);

# omitted_fk_closed_form('ema3_omitted_after', 0);
# omitted_fk_closed_form('ema3_omitted_after', 1);
# omitted_fk_closed_form('ema3_omitted_after', 2);
# omitted_fk_closed_form('ema3_omitted_after', 3);
# omitted_fk_closed_form('ema3_omitted_after', 4);
omitted_expr('ema3_omitted_after');


#------------------------------------------------------------------------------
print "\n\nLaguerre\n";

# From
#
#     L1 = -(1-alpha)*L0 + L0prev + (1-alpha)*L1prev
#
# rearrange to show it's an EMA,
#
#                  L0prev - f*L0
#     L1 = alpha * ------------- + (1-alpha)*L1prev
#                      (1-f)
#
# of the term
#
#     L0prev - f*L0
#     -------------
#         (1-f)
#

# (define (L1-pre-ema-term k)
#   (poly-div (poly-sub (if (zero? k)
# 			  '()
# 			  (L0-term (1- k)))
# 		      (poly-shift (L0-term k) 1))
# 	    '(1 -1)))

# poly equal to $X
my $poly_X = Math::Polynomial->new(Math::BigRat->new(0),
                                   Math::BigRat->new(1));
# poly equal to 1-$X
my $poly_1_minus_X = Math::Polynomial->new(Math::BigRat->new(1),
                                           Math::BigRat->new(-1));

sub make_laguerre_for_ema {
  my ($term_proc) = @_;
  return memoize
    (sub {
       my ($k) = @_;
       # say "make_laguerre_for_ema $k";
       my $L0prev = ($k <= 0 ? $poly_0 : $term_proc->($k-1));
       my $L0_times_f = $term_proc->($k) * $poly_X;
       my $diff = $L0_times_f - $L0prev;
       # $diff = $diff->div1c (1); # (x-1) instead of (1-x)
       $diff = $diff->div_root (1); # (x-1) instead of (1-x)
       # say "l done";
       return $diff;
     });
}

sub L0_term {
  my ($k) = @_;
  # say "L0_term $k";
  if ($k < 0) {
    return Math::BigRat->new(0);
  } else {
    return Math::Polynomial->new ((0) x $k,
                                  Math::BigRat->new(1),
                                  Math::BigRat->new(-1));
  }
}
memoize 'L0_term';
print_ma_terms ('L0_term');

*L1_pre_ema_term = make_laguerre_for_ema(\&L0_term);
*L1_term = ema_of_term_proc (\&L1_pre_ema_term);
print_ma_terms ('L1_term');

*L2_pre_ema_term = make_laguerre_for_ema(\&L1_term);
*L2_term = ema_of_term_proc (\&L2_pre_ema_term);
print_ma_terms ('L2_term');

*L3_pre_ema_term = make_laguerre_for_ema(\&L2_term);
*L3_term = ema_of_term_proc (\&L3_pre_ema_term);
print_ma_terms ('L3_term');

sub laguerre_term {
  my ($k) = @_;
  return (L0_term($k) + 2*L1_term($k) + 2*L2_term($k) + L3_term($k))
    / 6;
}
memoize ('laguerre_term');
print_ma_terms ('laguerre_term');

*laguerre_weight_to = make_weight_proc(\&laguerre_term);
say "laguerre_weight_to(4) ", laguerre_weight_to(4);

sub laguerre_omitted_after {
  my ($k) = @_;
  # say "laguerre_omitted_after $k";
  return 1 - laguerre_weight_to($k);
}
memoize 'laguerre_omitted_after';
say "laguerre_omitted_after(4)  ",laguerre_omitted_after(4);

# omitted_fk_closed_form('laguerre_omitted_after', -2);
# omitted_fk_closed_form('laguerre_omitted_after', -1);
# omitted_fk_closed_form('laguerre_omitted_after', 0);
# omitted_fk_closed_form('laguerre_omitted_after', 1);
# omitted_fk_closed_form('laguerre_omitted_after', 2);
# omitted_fk_closed_form('laguerre_omitted_after', 3);
# omitted_fk_closed_form('laguerre_omitted_after', 4);
omitted_expr('laguerre_omitted_after');

exit 0;