App-Chart
view release on metacpan or search on metacpan
lib/App/Chart/Series/Derived/EMAx2.pm view on Meta::CPAN
use Locale::TextDomain 1.17; # for __p()
use Locale::TextDomain ('App-Chart');
use base 'App::Chart::Series::Indicator';
use App::Chart::Series::Derived::EMA;
sub longname { __('EMA of EMA') }
sub shortname { __('EMAofEMA') }
sub manual { __p('manual-node','EMA of EMA') }
use constant
{ type => 'average',
priority => -10,
parameter_info => [ { name => __('Days'),
key => 'ema2_days',
type => 'float',
minimum => 1,
default => 20,
decimals => 0,
step => 1 } ],
};
sub new {
my ($class, $parent, $N) = @_;
$N //= parameter_info()->[0]->{'default'};
($N > 0) or croak "EMA2 bad N: $N";
return $class->SUPER::new
(parent => $parent,
parameters => [ $N ],
N => $N,
arrays => { values => [] },
array_aliases => { });
}
# Return a procedure which calculates an EMA of EMA with given $N period
# smoothing (on both).
#
# Each call $proc->($value) enters a new value into the window, and the
# return is the EMAofEMA up to (and including) that value.
#
# An EMA of EMA is in theory influenced by all preceding data, but
# warmup_count() below is designed to determine a warmup count.
#
sub proc {
my ($class_or_self, $N) = @_;
my $ema_proc = App::Chart::Series::Derived::EMA->proc ($N);
my $ema2_proc = App::Chart::Series::Derived::EMA->proc ($N);
return sub { $ema2_proc->($ema_proc->($_[0])) };
}
# By priming an EMA-2 pro with warmup_count() many values, the next call
# will have an omitted weight of no more than 0.1% of the total. Omitting
# 0.1% should be negligable, unless past values are ridiculously bigger than
# recent ones.
#
# The implementation here does a binary search for the first k satisfying
# R(k)<=0.001, so it's only moderately fast. Perhaps there'd be a direct
# closed-form solution to the equation R(k)=0.001 below. The inverse of
# k*f^k is close to the Lambert W-function. Is there an easy formula for
# that?
#
sub warmup_count {
my ($self_or_class, $N) = @_;
if ($N <= 1) { return 0; }
my $f = App::Chart::Series::Derived::EMA::N_to_f ($N);
return bsearch_first_true
(sub {
my ($i) = @_;
return (ema2_omitted($f,$i)
<= App::Chart::Series::Derived::EMA::WARMUP_OMITTED_FRACTION);
},
$N);
}
# ema2_omitted() returns the fraction (between 0 and 1) of weight omitted
# by stopping an EMA of EMA at the f^k term, which means the first k+1
# terms.
#
# The total weight up to that term is,
#
# W(k) = (1-f)^2 * ( 1 + 2f + 3f^2 + 4f^3 + ... + (k+1)*f^k )
#
# Multiplying (1-f)^2 through leads to the middle terms cancelling, leaving
# just the 1 at the start, and two terms at the end,
#
# W(k) = 1 + (-2*(k+1) + k) * f^(k+1)
# + (k+1) * f^(k+2)
#
# The omitted part is 1 - W(k), so the "1 +" is dropped and the rest
# negated. The terms simplify to
#
# R(k) = f^(k+1) * (k+2 - f * (k+1))
#
# See devel/ema-omitted.pl for automated checking of this calculation.
#
sub ema2_omitted {
my ($f, $k) = @_;
return $f**($k+1) * ($k+2 - $f*($k+1));
}
sub bsearch_first_true {
my ($pred, $incr) = @_;
$incr = POSIX::ceil ($incr);
my $lo = 0;
my $hi = $incr;
# search upwards
until ($pred->($hi)) {
$lo = $hi + 1;
$hi *= 2;
}
# at this point $pred->($lo) is unknown, $pred->($hi) is true
if ($pred->($lo)) { return $lo; }
# binary search loop, with $pred->($lo) false, $pred->($hi) true
for (;;) {
( run in 0.584 second using v1.01-cache-2.11-cpan-39bf76dae61 )