File Coverage

blib/lib/App/Chart/Series/Derived/EMA.pm
Criterion Covered Total %
statement 20 20 100.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 27 27 100.0


line stmt bran cond sub pod time code
1             # Copyright 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2011 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17             package App::Chart::Series::Derived::EMA;
18 2     2   883 use 5.010;
  2         7  
19 2     2   8 use strict;
  2         3  
  2         35  
20 2     2   8 use warnings;
  2         4  
  2         50  
21 2     2   8 use Carp;
  2         2  
  2         92  
22 2     2   9 use POSIX ();
  2         3  
  2         28  
23 2     2   677 use Locale::TextDomain ('App-Chart');
  2         30563  
  2         14  
24              
25 2     2   12084 use base 'App::Chart::Series::Indicator';
  2         6  
  2         948  
26             use App::Chart::Series::Calculation;
27              
28              
29             # In the manual it's noted that the first n days weight make up 86.5% of
30             # the total weight in an EMA. That amount is x = 1 + f + f^2 + ... +
31             # f^(n-1), and for total weight t
32             #
33             # t = x + f^n*(1 + f + f^2 + ...)
34             #
35             # t = x + f^n*t
36             #
37             # so the fraction of the total is
38             #
39             # x/t = 1 - f^n
40             #
41             # / 2 \ n
42             # = 1 - | 1 - --- |
43             # \ n+1 /
44             #
45             # / -2 \ n+1
46             # | 1 + --- |
47             # \ n+1 /
48             # = 1 - -----------
49             # / 2 \
50             # | 1 - --- |
51             # \ n+1 /
52             #
53             # As n increases, the numerator approaches e^-2 from the limit (1+x/n)^n
54             # --> e^x by Euler, and the numerator approaches 1. So the result is
55             #
56             # 1
57             # x/t --> 1 - --- = 0.8646647...
58             # e^2
59             #
60              
61             sub longname { __('EMA - Exponential MA') }
62             sub shortname { __('EMA') }
63             sub manual { __p('manual-node','Exponential Moving Average') }
64              
65             use constant
66             { priority => 12,
67             type => 'average',
68             parameter_info => [ { name => __('Days'),
69             key => 'ema_days',
70             type => 'float',
71             minimum => 1,
72             default => 20,
73             decimals => 0,
74             step => 1 } ],
75             };
76              
77             sub new {
78             my ($class, $parent, $N) = @_;
79              
80             $N //= parameter_info()->[0]->{'default'};
81             ($N > 0) or croak "EMA bad N: $N";
82              
83             return $class->SUPER::new
84             (parent => $parent,
85             parameters => [ $N ],
86             N => $N,
87             arrays => { values => [] },
88             array_aliases => { });
89             }
90              
91             # Return a procedure which calculates an exponential moving average over an
92             # accumulated window.
93             #
94             # Each call $proc->($value) enters a new value into the window, and the
95             # return is the EMA up to (and including) that $value.
96             #
97             # An EMA is in theory influenced by all preceding data, but warmup_count()
98             # below is designed to determine a warmup count. By calling $proc with
99             # warmup_count($N) many values, the next call will have an omitted weight of
100             # no more than 0.1% of the total. Omitting 0.1% should be negligable,
101             # unless past values are ridiculously bigger than recent ones.
102             #
103             sub proc {
104             my ($self_or_class, $N) = @_;
105              
106             if ($N <= 1) {
107             return \&App::Chart::Series::Calculation::identity;
108             }
109              
110             # $sum is v0 + v1*f + v2*f^2 + v3*f^3 + ... + vk*f^k, for as many $value's
111             # as so far entered
112             #
113             # $weight is the corresponding 1 + f + f^2 + ... + f^k. This approaches
114             # 1/(1-f), but on the first few outputs it's much smaller, so must
115             # calculate it explicitly.
116              
117             my $f = N_to_f ($N);
118             my $alpha = N_to_alpha ($N);
119             my $sum = 0;
120             my $weight = 0;
121             return sub {
122             my ($value) = @_;
123             $sum = $sum * $f + $value * $alpha;
124             $weight = $weight * $f + $alpha;
125             return $sum / $weight;
126             };
127             }
128              
129             # By priming an EMA accumulator PROC above with warmup_count($N) many
130             # values, the next call will have an omitted weight of no more than 0.1% of
131             # the total. Omitting 0.1% should be negligable, unless past values are
132             # ridiculously bigger than recent ones. The implementation is fast, per
133             # ema_omitted_search() below.
134             #
135             # Knowing that log(f) approaches -2/count as count increases, the result
136             # from ema_omitted_search() is roughly log(0.001)/(-2/$N) = 3.45*$N.
137             #
138             use constant WARMUP_OMITTED_FRACTION => 0.001;
139              
140             sub warmup_count {
141             my ($self_or_class, $N) = @_;
142             if ($N <= 1) {
143             return 0;
144             } else {
145             return ema_omitted_search (N_to_f($N), WARMUP_OMITTED_FRACTION) - 1 ;
146             }
147             }
148              
149             # ema_omitted_search() returns the number of terms t needed in an EMA to
150             # have an omitted part <= TARGET, where target is a proportion between 0 and
151             # 1. This means
152             #
153             # Omitted(t-1) <= target
154             # f^t <= target
155             # t >= log(target) / log(f)
156             #
157             # Can have f==0 when count==1 (a degenerate EMA, which just follows the
158             # given points exactly). log(0) isn't supported on guile 1.6, hence the
159             # special case.
160             #
161             # Actually log(f) approaches -2/N as N increases, but it's easy enough to
162             # do the calculation exactly.
163             #
164             sub ema_omitted_search {
165             my ($f, $target) = @_;
166             if ($f == 0) {
167             return 0;
168             } else {
169             return POSIX::ceil (log($target) / log($f));
170             }
171             }
172              
173             # ema_omitted() returns the fraction (between 0 and 1) of weight omitted by
174             # stopping an EMA at the f^k term, which means the first k+1 terms.
175             #
176             # The weight, out of a total 1, in those first terms
177             #
178             # W(k) = (1-f) (1 + f + f^2 + ... + f^k)
179             #
180             # multiplying through makes the middle terms cancel, leaving
181             #
182             # W(k) = 1 - f^(k+1)
183             #
184             # The omitted part is then O = 1-W,
185             #
186             # Omitted(k) = f^(k+1)
187             #
188             sub ema_omitted {
189             my ($f, $k) = @_;
190             return $f ** ($k + 1);
191             }
192              
193             # alpha=2/(N+1)
194             sub N_to_alpha {
195             my ($N) = @_;
196             return 2 / ($N + 1);
197             }
198             # f=1-2/(N+1), rearranged to f=(N-1)/(N+1).
199             sub N_to_f {
200             my ($N) = @_;
201             return ($N - 1) / ($N + 1);
202             }
203             # N = 2/alpha - 1
204             sub alpha_to_N {
205             my ($alpha) = @_;
206             return 2 / $alpha - 1;
207             }
208             # convert a $N in J. Welles Wilder's reckoning to one in the standard form
209             # Wilder alpha=1/W, alpha=2/(N+1), so N=2*W-1
210             sub N_from_Wilder_N {
211             my ($W) = @_;
212             return 2*$W - 1;
213             }
214             sub N_to_Wilder_N {
215             my ($N) = @_;
216             return ($N+1)/2;
217             }
218              
219             1;
220             __END__
221              
222             # =head1 NAME
223             #
224             # App::Chart::Series::Derived::EMA -- exponential moving average
225             #
226             # =head1 SYNOPSIS
227             #
228             # my $series = $parent->EMA($N);
229             #
230             # =head1 DESCRIPTION
231             #
232             # ...
233             #
234             # =head1 SEE ALSO
235             #
236             # L<App::Chart::Series>, L<App::Chart::Series::Derived::SMA>
237             #
238             # =cut