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, 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 this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;
use Carp;
use Data::Dumper;
use Getopt::Long;
use List::Util qw(min max);
use POSIX qw(floor ceil);

my $option_verbose = 0;
my $option_txt = 1;
my $option_png = 1;
my $option_eps = 0;


GetOptions ('eps' => sub {
              $option_eps = 1;
              $option_png = 0;
              $option_txt = 0;
            })
  or exit 1;


#-----------------------------------------------------------------------------
# misc

sub write_file {
  my ($filename, $content) = @_;
  open my $out, '>', $filename or die;
  print $out $content or die;
  close $out or die;
}

#-----------------------------------------------------------------------------
# text graph

# (define (multiple? n d)
#   (integer? (/ n d)))

sub text_plot {
  my ($basename, $data) = @_;

  my $y_zero_pos = 20;
  my $x_step     = (@$data < 30 ? 2 : 1);
  my $x_max      = @$data + 3;
  my $width      = $x_step * ($x_max + 1);
  my $data_max   = max (@$data);
  my $data_min   = min (@$data);
  my $data_range = $data_max - $data_min;

  $data_max     += $data_range * 0.05;
  $data_min     -= $data_range * 0.05;
  $data_range = $data_max - $data_min;

  my $y_factor     = ($data_range < 11 ? 2.0
                      : $data_range < 22 ? 1.0
                      : 0.5);
  my $y_tick_step  = ($y_factor >= 2.0 ? 2
                      : $y_factor >= 1.0 ? 5
                      : 10);
  my $out_x_base = -4;
  my $out_y_base = -30;
  my @out;
  #   foreach my $out            (make_array #\space '(_4 75) '(_30 60))))

  my $setchar = sub {
    my ($x, $y, $char) = @_;
    $out[$y - $out_y_base][$x - $out_x_base] = $char;
  };
  my $setstr = sub {
    my ($x, $y, $str) = @_;
    for (my $i = 0; $i < length($str); $i++) {
      $setchar->($x+$i, $y, substr($str, $i, 1));
    }
  };
  my $scale_y = sub {
    my ($ydat) = @_;
    return $y_zero_pos + ceil($y_factor * $ydat);
  };
  my $scale_x = sub {
    my ($x) = @_;
    return $x * $x_step + floor(0.5 * $x_step);
  };

  # horizontal axis
  foreach my $i ($scale_x->(0) - 1 .. $width) {
    $setchar->($i, $scale_y->(0), '-');
  }

  # horizontal ticks
  foreach my $x (0 .. $x_max) {
    if (($x % 5) == 0) {
      $setchar->($scale_x->($x), $scale_y->(0), '+');
    }
  }

  # horizontal scale numbers
  foreach my $x (0 .. $x_max) {
    if (($x % 5) == 0
        && ($data->[$x]//0) >= 0) {
      $setstr->($scale_x->($x), $scale_y->(0) - 1, $x);
    }
  }

  # data
  foreach my $x (0 .. $#$data) {
    my $ydat = $data->[$x];
    my $ypos = $scale_y->($ydat);
    if ($ydat < 0) {
      for (my $j = $scale_y->(0); $j >= $ypos; $j--) {
        $setchar->($scale_x->($x), $j, '*');
      }
    } else {
      for (my $j = $scale_y->(0); $j <= $ypos; $j++) {
        $setchar->($scale_x->($x), $j, '*');
      }
    }
  }

  # vertical axis, positive
  { my $y = 0;
    for ( ; $y < $data_max + 0.1 * $data_range; $y += 0.5) {
      $setchar->(-1, $scale_y->($y), '|');
    }
    $setchar->(-3, $scale_y->($y) - 1, '%');
  }

  # vertical scale numbers, positive
  for (my $y = 0; $y < $data_max + 0.1 * $data_range; $y += $y_tick_step) {
    $setchar->(-1, $scale_y->($y), '+');
    $setstr->(-4, $scale_y->($y), sprintf("%2.0f", $y));
  }

  # vertical axis, negative
  if (List::Util::first {$_ < 0} @$data) {
    # vertical axis, negative
    for (my $y = 0; $y > $data_min; $y -= 0.5) {
      $setchar->(-1, $scale_y->($y), '|');
    }

    # vertical scale ticks, negative
    for (my $y = 0; $y > $data_min; $y -= $y_tick_step) {
      $setchar->(-1, $scale_y->($y), '+');
    }
  }

  my $str;
  foreach my $row (reverse @out) {
    if (defined $row) {
      $str .= join ('', map {$_ // ' '} @$row);
    }
    $str .= "\n";
  }
  $str =~ s/ +\n/\n/g;
  $str =~ s/^\n+//;
  $str =~ s/\n+$/\n/;

  if ($option_verbose) {
    print $str;
  }
  write_file ("$basename.txt", $str);
}
# foreach my
#     (let ((lst (array_>list out)))
#       (set! lst (apply zip lst))
#       (set! lst (map! list_>string lst))
#       (set! lst (reverse! lst))
#       (set! lst (map! string_trim_right lst))
#       (set! lst (drop_while string_null? lst))
#       (set! lst (drop_right_while string_null? lst))
#
#       (let ((str (string_join lst "\n" 'suffix)))
# 	(if option_verbose
# 	    (display str))
# 	(call_with_output_file (string_append basename ".txt")
# 	  (lambda (port)
# 	    (display str port)))))))


# (define data '(1 2 3 4 5 6 7 6 5 4 0 0 0 0 0 0))
# (let ((total (apply + data)))
#   (set! data (map (lambda (x)
# 		    (* 100.0 (/ x total)))
# 		  data)))
# (dv data)
# (text_plot "foo" data)
# (exit 0)



#_____________________________________________________________________________

sub gnuplot_run {
  my ($basename, $data) = @_;
  print "gnuplot $basename\n";

  my $data_max   = max (@$data);
  my $data_min   = min (@$data);
  my $data_range = $data_max - $data_min;

  my $xhigh      = @$data + 0.5;
  my $datafilename = "$basename.data";
  my $plotfilename = "weights.gnuplot";

  $data_max     += $data_range * 0.1;
  $data_min     -= $data_range * 0.1;
  $data_range = $data_max - $data_min;

  while (@$data && $data->[-1] == 0) {
    pop @$data;
  }
  write_file ($datafilename, join ('', map {; "$_\n"} @$data));

  # png
  #
  if ($option_png) {
    # something evil happens with "xtics axis", need dummy xlabel
    write_file ($plotfilename, <<"HERE");
set terminal png size 400,250

# there was some sort of incompatible change in gnuplot 4.2 forcing the
# ``offset'' keyword here, dunno if it works with older gnuplot too ...
set xlabel " " offset 0, -2
set xrange [-0.5:$xhigh]
set xtics axis 5
set mxtics 5

set yrange [$data_min:$data_max]
set format y "%.1f"

unset key
set style fill solid 1.0
set boxwidth 0.6 relative
plot "$datafilename" with boxes
HERE

    system("gnuplot $plotfilename 2>&1 >$basename.png") == 0 or die;
  }

  # eps
  #
  if ($option_eps) {
    # something evil happens with "xtics axis", need dummy xlabel
    write_file ($plotfilename, <<"HERE");
set terminal postscript portrait

set xlabel " " offset 0, -2
set xrange [-0.5:$xhigh]
set xtics axis 5
set mxtics 5

set yrange [$data_min:$data_max]
set format y "%.1f"

unset key
set style fill solid 1.0
set boxwidth 0.6 relative
plot "$datafilename" with boxes
HERE

    system ("gnuplot $plotfilename 2>&1 >$basename.eps") == 0 or die;
  }

  unlink ($datafilename) or die;
  unlink ($plotfilename) or die;
}


sub mung_png {
  my ($filename, $title) = @_;
  require Image::ExifTool;

  # allow writing of extra png "Homepage" field, if not already setup
  { no warnings 'once';
    $Image::ExifTool::UserDefined
      {'Image::ExifTool::PNG::TextualData'}
        {'Homepage'} ||= {};
  }

  my $exif = Image::ExifTool->new;
  $exif->ExtractInfo($filename) or die;

  $exif->SetNewValue ('Title', $title);
  $exif->SetNewValue ('Author', 'Kevin Ryde');

  $exif->SetNewValue ('Copyright', <<'HERE');
Copyright 2007, 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 this program.  If not, see <http://www.gnu.org/licenses/>.
HERE

  $exif->SetNewValue ('CreationTime',
                      POSIX::strftime ('%a, %d %b %Y %H:%M:%S %z',
                                       localtime(time)));
  $exif->SetNewValue ('Software', 'Chart doc/weights.pl, and gnuplot');

  $exif->SetNewValue
    ('Homepage', 'http://user42.tuxfamily.org/chart/index.html');

  $exif->WriteInfo($filename) or die;
}

sub weights {
  my %opt = @_;
  my $description = $opt{'description'}
    || croak "weights: missing 'description'";
  my $basename = $opt{'basename'}
    || croak "weights: missing 'basename'";
  my $method = $opt{'method'};
  my $parameters = $opt{'parameters'} || [ $opt{'N'} ];
  my $show_count = $opt{'show_count'};
  # (proc (calc_proc count))

  my $warmup = 30 * $show_count;
  my @input = ((0) x $warmup,
               100,
               (0) x ($show_count - 1));

  my $in_series = ConstantSeries->new (array => \@input);
  my $ma_series = $in_series->$method (@$parameters);
  my $hi = $ma_series->hi;
  $ma_series->fill (0, $hi);

  my $output = $ma_series->array($opt{'array'}||'values');
  my @weights = @{$output}[$warmup .. $hi];

  if ($option_verbose) {
    print "$basename: ",Data::Dumper->Dump([\@weights],['weights']);
  }

  if (abs($weights[-1]) >= 1) {
    print "$basename: last weight $weights[-1]\n";
#    exit 1;
  }

  if ($option_txt) {
    text_plot ($basename, \@weights);
  }
  if ($option_png || $option_eps) {
    gnuplot_run ($basename, \@weights);
  }
  if ($option_png) {
    my $params = ($opt{'N'} ? "N=$opt{'N'}" : join(',', @$parameters));
    mung_png ("$basename.png", "$description, $params")
  }
}


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

# weights (description => "MACD weights",
#          basename    => "chart-macd-weights",
#          method      => 'MACD',
#          parameters  => [12,26],
#          show_count  => 40);
# 
# weights (description => "MACD histogram weights",
#          basename    => "chart-macd-histogram-weights",
#          method      => 'MACD',
#          array       => 'histogram',
#          parameters  => [12,26,9],
#          show_count  => 40);


weights (description => "Exponential moving average weights",
         basename    => "chart-ema-weights",
         method      => 'EMA',
         N           => 15,
         show_count  => 30);

weights (description => "EMA of EMA weights",
         basename    => "chart-ema-2-weights",
         method      => sub {
           my ($parent, $N) = @_;
           return $parent->EMA($N)->EMA($N);
         },
         N           => 10,
         show_count  => 30);

weights (description => "EMA of EMA of EMA weights",
         basename    => "chart-ema-3-weights",
         method      => sub {
           my ($parent, $N) = @_;
           return $parent->EMA($N)->EMA($N)->EMA($N);
         },
         N           => 10,
         show_count  => 38);

weights (description => "Endpoint moving average weights",
         basename    => "chart-epma-weights",
         method      => 'EPMA',
         N           => 15,
         show_count  => 18);

weights (description => "Hull moving average weights",
         basename    => "chart-hull-weights",
         method      => 'HullMA',
         N           => 15,
         show_count  => 20);

weights (description => "Double-exponential moving average weights",
         basename    => "chart-dema-weights",
         method      => 'DEMA',
         N           => 20,
         show_count  => 40);

weights (description => "DEMA of DEMA of DEMA weights",
         basename    => "chart-dema-3-weights",
         method      => sub { $_[0]->DEMA($_[1])->DEMA($_[1])->DEMA($_[1]) },
         N           => 10,
         show_count  => 40);

weights (description => "Laguerre filter weights",
         basename    => "chart-laguerre-weights",
         method      => 'LaguerreFilter',
         N           => 0.2,
         show_count  => 40);

weights (description => "Regularized EMA weights",
         basename    => "chart-rema-weights",
         method      => 'REMA',
         N           => 15,  # with default lambda=0.5
         show_count  => 30);

weights (description => "Sine moving average weights",
         basename    => "chart-sine-weights",
         method      => 'SineMA',
         N           => 10,
         show_count  => 12);

weights (description => "T3 moving average weights",
         basename    => "chart-t3-weights",
         method      => 'T3',  # with default vf==0.7
         N           => 10,
         show_count  => 40);

weights (description => "Triangular moving average weights",
         basename    => "chart-tma-weights",
         method      => 'TMA',
         N           => 15,
         show_count  => 18);

weights (description => "Triple-exponential moving average weights",
         basename    => "chart-tema-weights",
         method      => 'TEMA',
         N           => 20,
         show_count  => 30);

weights (description => "Weighted moving average weights",
         basename    => "chart-wma-weights",
         method      => 'WMA',
         N           => 15,
         show_count  => 18);

weights (description => "Zero_lag exponential moving average weights",
         basename    => "chart-zlema-weights",
         method      => 'ZLEMA',
         N           => 15,
         show_count  => 20);

exit 0;


package ConstantSeries;
use strict;
use warnings;
use base 'App::Chart::Series';

sub new {
  my ($class, %option) = @_;
  my $array = delete $option{'array'} || die;
  $option{'hi'} = $#$array;
  $option{'name'} //= 'Const';
  $option{'timebase'} ||= do {
    require App::Chart::Timebase::Days;
    App::Chart::Timebase::Days->new_from_iso ('2008-07-23')
    };
  return $class->SUPER::new (arrays => { values => $array },
                             %option);
}
sub fill_part {}

__END__