| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # Copyright 2007, 2008, 2009, 2010, 2011, 2016 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; | 
| 18 | 22 |  |  | 22 |  | 9688 | use 5.010; | 
|  | 22 |  |  |  |  | 183 |  | 
| 19 | 22 |  |  | 22 |  | 179 | use strict; | 
|  | 22 |  |  |  |  | 50 |  | 
|  | 22 |  |  |  |  | 531 |  | 
| 20 | 22 |  |  | 22 |  | 125 | use warnings; | 
|  | 22 |  |  |  |  | 45 |  | 
|  | 22 |  |  |  |  | 682 |  | 
| 21 | 22 |  |  | 22 |  | 131 | use Carp; | 
|  | 22 |  |  |  |  | 42 |  | 
|  | 22 |  |  |  |  | 1852 |  | 
| 22 | 22 |  |  | 22 |  | 149 | use List::Util qw(min max); | 
|  | 22 |  |  |  |  | 51 |  | 
|  | 22 |  |  |  |  | 2399 |  | 
| 23 | 22 |  |  | 22 |  | 7020 | use Locale::TextDomain ('App-Chart'); | 
|  | 22 |  |  |  |  | 328704 |  | 
|  | 22 |  |  |  |  | 153 |  | 
| 24 | 22 |  |  | 22 |  | 124734 | use Set::IntSpan::Fast 1.10;    # 1.10 for contains_all_range | 
|  | 22 |  |  |  |  | 184276 |  | 
|  | 22 |  |  |  |  | 788 |  | 
| 25 |  |  |  |  |  |  |  | 
| 26 | 22 |  |  | 22 |  | 8746 | use App::Chart; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  |  | 
| 28 |  |  |  |  |  |  | # uncomment this to run the ### lines | 
| 29 |  |  |  |  |  |  | #use Devel::Comments '###'; | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | # defaults | 
| 32 |  |  |  |  |  |  | use constant { default_linestyle => 'Line', | 
| 33 |  |  |  |  |  |  | dividends      => [], | 
| 34 |  |  |  |  |  |  | splits         => [], | 
| 35 |  |  |  |  |  |  | annotations    => [], | 
| 36 |  |  |  |  |  |  | parameter_info => [], | 
| 37 |  |  |  |  |  |  | minimum        => undef, | 
| 38 |  |  |  |  |  |  | maximum        => undef, | 
| 39 |  |  |  |  |  |  | hlines         => [], | 
| 40 |  |  |  |  |  |  | units          => '', | 
| 41 |  |  |  |  |  |  | }; | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | sub new { | 
| 44 |  |  |  |  |  |  | #### Series new: @_ | 
| 45 |  |  |  |  |  |  | my $class = shift; | 
| 46 |  |  |  |  |  |  | my $self = bless { fill_set => Set::IntSpan::Fast->new, | 
| 47 |  |  |  |  |  |  | @_ }, $class; | 
| 48 |  |  |  |  |  |  | if (my $parent = $self->{'parent'}) { | 
| 49 |  |  |  |  |  |  | $self->{'arrays'} ||= { map {; $_ => [] } | 
| 50 |  |  |  |  |  |  | keys %{$parent->{'arrays'}} }; | 
| 51 |  |  |  |  |  |  | $self->{'array_aliases'} ||= $parent->{'array_aliases'}; | 
| 52 |  |  |  |  |  |  | } | 
| 53 |  |  |  |  |  |  | $self->{'array_aliases'} ||= {}; | 
| 54 |  |  |  |  |  |  | return $self; | 
| 55 |  |  |  |  |  |  | } | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | sub name { | 
| 58 |  |  |  |  |  |  | my ($self) = @_; | 
| 59 |  |  |  |  |  |  | return $self->{'name'}; | 
| 60 |  |  |  |  |  |  | } | 
| 61 |  |  |  |  |  |  | sub parent { | 
| 62 |  |  |  |  |  |  | my ($self) = @_; | 
| 63 |  |  |  |  |  |  | return $self->{'parent'}; | 
| 64 |  |  |  |  |  |  | } | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | sub timebase { | 
| 67 |  |  |  |  |  |  | my ($self) = @_; | 
| 68 |  |  |  |  |  |  | return ($self->{'timebase'} || $self->{'parent'}->timebase); | 
| 69 |  |  |  |  |  |  | } | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | sub decimals { | 
| 72 |  |  |  |  |  |  | my ($self) = @_; | 
| 73 |  |  |  |  |  |  | if (exists $self->{'decimals'}) { return $self->{'decimals'}; } | 
| 74 |  |  |  |  |  |  | return $self->{'parent'}->decimals; | 
| 75 |  |  |  |  |  |  | } | 
| 76 |  |  |  |  |  |  |  | 
| 77 |  |  |  |  |  |  | sub symbol { | 
| 78 |  |  |  |  |  |  | my ($self) = @_; | 
| 79 |  |  |  |  |  |  | if (exists $self->{'symbol'}) { return $self->{'symbol'}; } | 
| 80 |  |  |  |  |  |  | if (my $parent = $self->{'parent'}) { return $parent->symbol; } | 
| 81 |  |  |  |  |  |  | return undef; | 
| 82 |  |  |  |  |  |  | } | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | sub hi { | 
| 85 |  |  |  |  |  |  | my ($self) = @_; | 
| 86 |  |  |  |  |  |  | if (exists $self->{'hi'}) { return $self->{'hi'}; } | 
| 87 |  |  |  |  |  |  | return $self->{'parent'}->hi; | 
| 88 |  |  |  |  |  |  | } | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  | sub values_array { | 
| 91 |  |  |  |  |  |  | my ($self) = @_; | 
| 92 |  |  |  |  |  |  | return $self->array('values'); | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  | sub array { | 
| 95 |  |  |  |  |  |  | my ($self, $aname) = @_; | 
| 96 |  |  |  |  |  |  | return ($self->{'arrays'}->{$aname} | 
| 97 |  |  |  |  |  |  | || do { my $alias = $self->{'array_aliases'}->{$aname}; | 
| 98 |  |  |  |  |  |  | $alias && $self->{'arrays'}->{$alias} }); | 
| 99 |  |  |  |  |  |  | # croak "No such array in $self: $aname"); | 
| 100 |  |  |  |  |  |  | } | 
| 101 |  |  |  |  |  |  | sub array_names { | 
| 102 |  |  |  |  |  |  | my ($self) = @_; | 
| 103 |  |  |  |  |  |  | return keys %{$self->{'arrays'}}; | 
| 104 |  |  |  |  |  |  | } | 
| 105 |  |  |  |  |  |  |  | 
| 106 |  |  |  |  |  |  | sub fill { | 
| 107 |  |  |  |  |  |  | my ($self, $lo, $hi) = @_; | 
| 108 |  |  |  |  |  |  | if ($lo > $hi) { croak "Series fill lo>hi ($lo, $hi)\n"; } | 
| 109 |  |  |  |  |  |  | if ($hi < 0) { return; }      # nothing | 
| 110 |  |  |  |  |  |  | { | 
| 111 |  |  |  |  |  |  | my $sh = $self->hi; | 
| 112 |  |  |  |  |  |  | $lo = min ($sh, max (0, $lo)); | 
| 113 |  |  |  |  |  |  | $hi = min ($sh, $hi); | 
| 114 |  |  |  |  |  |  | } | 
| 115 |  |  |  |  |  |  |  | 
| 116 |  |  |  |  |  |  | my $got_set = $self->{'fill_set'}; | 
| 117 |  |  |  |  |  |  | if ($got_set->contains_all_range ($lo, $hi)) { return; } # covered already | 
| 118 |  |  |  |  |  |  |  | 
| 119 |  |  |  |  |  |  | my $want_set = Set::IntSpan::Fast->new; | 
| 120 |  |  |  |  |  |  | $want_set->add_range ($lo, $hi); | 
| 121 |  |  |  |  |  |  | $want_set = $want_set->diff ($got_set); | 
| 122 |  |  |  |  |  |  | ### fill: "$lo-$hi of ".ref($self) | 
| 123 |  |  |  |  |  |  | ### want: $want_set->as_string." on top of ".$got_set->as_string | 
| 124 |  |  |  |  |  |  |  | 
| 125 |  |  |  |  |  |  | # merge now so don't repeat if error in code below | 
| 126 |  |  |  |  |  |  | $got_set->merge ($want_set); | 
| 127 |  |  |  |  |  |  | ### merge to: $got_set->as_string | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | my $values = $self->values_array; | 
| 130 |  |  |  |  |  |  | my @array_names = $self->array_names; | 
| 131 |  |  |  |  |  |  | if ($self->array('volumes') && $self->array('volumes') != $values) { | 
| 132 |  |  |  |  |  |  | @array_names = grep {$_ ne 'volumes'} @array_names; | 
| 133 |  |  |  |  |  |  | } | 
| 134 |  |  |  |  |  |  | if ($self->array('openints') && $self->array('openints') != $values) { | 
| 135 |  |  |  |  |  |  | @array_names = grep {$_ ne 'openints'} @array_names; | 
| 136 |  |  |  |  |  |  | } | 
| 137 |  |  |  |  |  |  | my @arrays = map {$self->array($_)} @array_names; | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | my $method = ($self->can('fill_part') | 
| 140 |  |  |  |  |  |  | || ($self->can('proc') && 'fill_part_from_proc') | 
| 141 |  |  |  |  |  |  | || die "Series $self has no fill method"); | 
| 142 |  |  |  |  |  |  |  | 
| 143 |  |  |  |  |  |  | my $iter = $want_set->iterate_runs; | 
| 144 |  |  |  |  |  |  | while (my ($lo, $hi) = $iter->()) { | 
| 145 |  |  |  |  |  |  | ### do: "$method $lo, $hi" | 
| 146 |  |  |  |  |  |  | $self->$method ($lo, $hi); | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | foreach my $array (@arrays) { | 
| 149 |  |  |  |  |  |  | $self->{'fill_high'} = App::Chart::max_maybe ($self->{'fill_high'}, | 
| 150 |  |  |  |  |  |  | @{$array}[$lo .. $hi]); | 
| 151 |  |  |  |  |  |  | $self->{'fill_low'} = App::Chart::min_maybe ($self->{'fill_low'}, | 
| 152 |  |  |  |  |  |  | @{$array}[$lo .. $hi]); | 
| 153 |  |  |  |  |  |  | } | 
| 154 |  |  |  |  |  |  | ### merge to: $got_set->as_string | 
| 155 |  |  |  |  |  |  | } | 
| 156 |  |  |  |  |  |  | } | 
| 157 |  |  |  |  |  |  |  | 
| 158 |  |  |  |  |  |  | sub range { | 
| 159 |  |  |  |  |  |  | my ($self, $lo, $hi, @array_names) = @_; | 
| 160 |  |  |  |  |  |  | ### Series range: "$lo $hi of @array_names" | 
| 161 |  |  |  |  |  |  | $lo = max ($lo, 0); | 
| 162 |  |  |  |  |  |  | if ($hi < $lo) { return; }    # eg. lo==-5 hi==-1, no data before 0 | 
| 163 |  |  |  |  |  |  | $self->fill ($lo, $hi); | 
| 164 |  |  |  |  |  |  | my $arrays_hash = $self->{'arrays'}; # hash | 
| 165 |  |  |  |  |  |  | if (! @array_names) { | 
| 166 |  |  |  |  |  |  | @array_names = $self->range_default_names; | 
| 167 |  |  |  |  |  |  | } | 
| 168 |  |  |  |  |  |  | my @arefs = @{$arrays_hash}{@array_names}; # hash slice | 
| 169 |  |  |  |  |  |  |  | 
| 170 |  |  |  |  |  |  | require List::MoreUtils; | 
| 171 |  |  |  |  |  |  | return List::MoreUtils::minmax | 
| 172 |  |  |  |  |  |  | (grep {defined} map { @{$_}[$lo .. $hi] } @arefs); | 
| 173 |  |  |  |  |  |  | } | 
| 174 |  |  |  |  |  |  | sub range_default_names { | 
| 175 |  |  |  |  |  |  | my ($self) = @_; | 
| 176 |  |  |  |  |  |  | return keys %{$self->{'arrays'}};  # all arrays | 
| 177 |  |  |  |  |  |  | } | 
| 178 |  |  |  |  |  |  |  | 
| 179 |  |  |  |  |  |  | sub linestyle { | 
| 180 |  |  |  |  |  |  | my ($self, $newval) = @_; | 
| 181 |  |  |  |  |  |  | if (@_ >= 2) { | 
| 182 |  |  |  |  |  |  | $self->{'linestyle'} = $newval; | 
| 183 |  |  |  |  |  |  | } elsif ($self->{'linestyle'}) { | 
| 184 |  |  |  |  |  |  | return $self->{'linestyle'}; | 
| 185 |  |  |  |  |  |  | } else { | 
| 186 |  |  |  |  |  |  | return $self->default_linestyle; | 
| 187 |  |  |  |  |  |  | } | 
| 188 |  |  |  |  |  |  | } | 
| 189 |  |  |  |  |  |  | sub linestyle_class { | 
| 190 |  |  |  |  |  |  | my ($self) = @_; | 
| 191 |  |  |  |  |  |  | my $linestyle = $self->linestyle // return undef; | 
| 192 |  |  |  |  |  |  | return "App::Chart::Gtk2::LineStyle::$linestyle"; | 
| 193 |  |  |  |  |  |  | } | 
| 194 |  |  |  |  |  |  |  | 
| 195 |  |  |  |  |  |  | # Return (LOWER UPPER) which is a suggested initial Y-axis page range to | 
| 196 |  |  |  |  |  |  | # show for dates LO to HI.  This is for use both with | 
| 197 |  |  |  |  |  |  | # App::Chart::Series::OHLCVI and also any other series type without its own | 
| 198 |  |  |  |  |  |  | # specific style. | 
| 199 |  |  |  |  |  |  | # | 
| 200 |  |  |  |  |  |  | # As described in "Main Window" in the manual, the aim is to scale according | 
| 201 |  |  |  |  |  |  | # to apparent volatility, so that daily range or daily close-to-close change | 
| 202 |  |  |  |  |  |  | # are some modest fraction of the initial page.  In particular if the series | 
| 203 |  |  |  |  |  |  | # is just going sideways it's not zoomed out to try to fill the whole | 
| 204 |  |  |  |  |  |  | # screen.  The absolute price level is not used, so say bond prices which | 
| 205 |  |  |  |  |  |  | # hover near 100 still get scaled out to make typical daily changes of 0.1 | 
| 206 |  |  |  |  |  |  | # or whatever visible. | 
| 207 |  |  |  |  |  |  | # | 
| 208 |  |  |  |  |  |  | sub initial_range { | 
| 209 |  |  |  |  |  |  | my ($self, $lo, $hi) = @_; | 
| 210 |  |  |  |  |  |  | ### Series initial_range: "$lo to $hi   $self" | 
| 211 |  |  |  |  |  |  | $lo = max ($lo, 0); | 
| 212 |  |  |  |  |  |  | $hi = max ($hi, 0); | 
| 213 |  |  |  |  |  |  | $self->fill ($lo, $hi); | 
| 214 |  |  |  |  |  |  | my $highs = $self->array('highs') // []; | 
| 215 |  |  |  |  |  |  | my $lows  = $self->array('lows') // $highs; | 
| 216 |  |  |  |  |  |  | my $values = $self->values_array; | 
| 217 |  |  |  |  |  |  | my @diffs = (); | 
| 218 |  |  |  |  |  |  |  | 
| 219 |  |  |  |  |  |  | my $timebase = $self->timebase; | 
| 220 |  |  |  |  |  |  | my $latest; | 
| 221 |  |  |  |  |  |  | if ($self->units eq 'price' | 
| 222 |  |  |  |  |  |  | && (my $symbol = $self->symbol)) { | 
| 223 |  |  |  |  |  |  | $latest = defined $symbol && App::Chart::Latest->get($symbol); | 
| 224 |  |  |  |  |  |  | } | 
| 225 |  |  |  |  |  |  | if ($latest | 
| 226 |  |  |  |  |  |  | && defined $latest->{'high'} | 
| 227 |  |  |  |  |  |  | && defined $latest->{'low'} | 
| 228 |  |  |  |  |  |  | && defined (my $last_iso = $latest->{'last_date'})) { | 
| 229 |  |  |  |  |  |  | my $last_t = $timebase->from_iso_floor ($last_iso); | 
| 230 |  |  |  |  |  |  | if ($last_t >= $lo && $last_t <= $hi) { | 
| 231 |  |  |  |  |  |  | push @diffs, $latest->{'high'} - $latest->{'low'}; | 
| 232 |  |  |  |  |  |  | } | 
| 233 |  |  |  |  |  |  | } | 
| 234 |  |  |  |  |  |  |  | 
| 235 |  |  |  |  |  |  | # high to low ranges in ohlcv | 
| 236 |  |  |  |  |  |  | if ($highs != $lows) { | 
| 237 |  |  |  |  |  |  | foreach my $i ($lo .. $hi) { | 
| 238 |  |  |  |  |  |  | if (defined $highs->[$i] && defined $lows->[$i]) { | 
| 239 |  |  |  |  |  |  | my $diff = CORE::abs ($highs->[$i] - $lows->[$i]); | 
| 240 |  |  |  |  |  |  | if ($diff != 0) { | 
| 241 |  |  |  |  |  |  | push @diffs, $diff; | 
| 242 |  |  |  |  |  |  | } | 
| 243 |  |  |  |  |  |  | } | 
| 244 |  |  |  |  |  |  | } | 
| 245 |  |  |  |  |  |  | } | 
| 246 |  |  |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | # ENHANCE-ME: look at all parts of a multi-line like macd, bollinger, | 
| 248 |  |  |  |  |  |  | # guppy, etc | 
| 249 |  |  |  |  |  |  | #     (if (= 2 (array-rank array)) | 
| 250 |  |  |  |  |  |  | # 	# macd, not quite right | 
| 251 |  |  |  |  |  |  | # 	(set! array (make-shared-array-column array 0))) | 
| 252 |  |  |  |  |  |  |  | 
| 253 |  |  |  |  |  |  | # close to close ranges | 
| 254 |  |  |  |  |  |  | { | 
| 255 |  |  |  |  |  |  | my $prev; | 
| 256 |  |  |  |  |  |  | foreach my $i ($lo .. $hi) { | 
| 257 |  |  |  |  |  |  | my $value = $values->[$i]; | 
| 258 |  |  |  |  |  |  | if (! defined $value) { next; } | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | if (defined $prev) { | 
| 261 |  |  |  |  |  |  | my $diff = CORE::abs ($value - $prev); | 
| 262 |  |  |  |  |  |  | if ($diff != 0) { | 
| 263 |  |  |  |  |  |  | push @diffs, $diff; | 
| 264 |  |  |  |  |  |  | } | 
| 265 |  |  |  |  |  |  | } | 
| 266 |  |  |  |  |  |  | $prev = $value; | 
| 267 |  |  |  |  |  |  | } | 
| 268 |  |  |  |  |  |  | } | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | if (! @diffs) { | 
| 271 |  |  |  |  |  |  | ### no diffs for initial range: "$lo $hi" | 
| 272 |  |  |  |  |  |  | my ($l, $h) = $self->range ($lo, $hi); | 
| 273 |  |  |  |  |  |  | if (defined $l) { | 
| 274 |  |  |  |  |  |  | # for just a single close value pretend 20% around the value | 
| 275 |  |  |  |  |  |  | return $h * 0.8, $l / 0.8; | 
| 276 |  |  |  |  |  |  | } | 
| 277 |  |  |  |  |  |  | return; | 
| 278 |  |  |  |  |  |  | } | 
| 279 |  |  |  |  |  |  |  | 
| 280 |  |  |  |  |  |  | # page will show 25x the median range | 
| 281 |  |  |  |  |  |  | @diffs = sort {$a <=> $b} @diffs; | 
| 282 |  |  |  |  |  |  | my $page = 25 * $diffs[CORE::int ($#diffs / 2)]; | 
| 283 |  |  |  |  |  |  | ### initial page by: "25*median is $page" | 
| 284 |  |  |  |  |  |  |  | 
| 285 |  |  |  |  |  |  | # make page no more than twice upper,lower data range, so the | 
| 286 |  |  |  |  |  |  | # data is not less than half the window | 
| 287 |  |  |  |  |  |  | { my ($l, $h) = $self->range ($lo, $hi); | 
| 288 |  |  |  |  |  |  | if (defined $l) { | 
| 289 |  |  |  |  |  |  | ### series range: "$l to $h" | 
| 290 |  |  |  |  |  |  | $page = min ($page, 2 * ($h - $l)); | 
| 291 |  |  |  |  |  |  | } | 
| 292 |  |  |  |  |  |  | } | 
| 293 |  |  |  |  |  |  | ### shrink to use minimum half window: $page | 
| 294 |  |  |  |  |  |  |  | 
| 295 |  |  |  |  |  |  | # make page no smaller than last 1/2 of data, so that's visible | 
| 296 |  |  |  |  |  |  | { my ($l, $h) = $self->range (CORE::int (($lo + $hi) / 2), $hi); | 
| 297 |  |  |  |  |  |  | if ($l) { $page = max ($page, $h - $l); } | 
| 298 |  |  |  |  |  |  | } | 
| 299 |  |  |  |  |  |  | ### expand so last half data visible: $page | 
| 300 |  |  |  |  |  |  |  | 
| 301 |  |  |  |  |  |  | my ($l, $h); | 
| 302 |  |  |  |  |  |  | my $accumulate = sub { | 
| 303 |  |  |  |  |  |  | my ($value) = @_; | 
| 304 |  |  |  |  |  |  | ### accumulate: $value | 
| 305 |  |  |  |  |  |  | if (! defined $value) { return 1; } | 
| 306 |  |  |  |  |  |  | my $new_l = defined $l ? min ($value, $l) : $value; | 
| 307 |  |  |  |  |  |  | my $new_h = defined $h ? max ($value, $h) : $value; | 
| 308 |  |  |  |  |  |  | if ($new_h - $new_l <= $page) { | 
| 309 |  |  |  |  |  |  | $l = $new_l; | 
| 310 |  |  |  |  |  |  | $h = $new_h; | 
| 311 |  |  |  |  |  |  | } | 
| 312 |  |  |  |  |  |  | if (! defined $l) { | 
| 313 |  |  |  |  |  |  | $l = $new_l; | 
| 314 |  |  |  |  |  |  | $h = $new_l + $page; | 
| 315 |  |  |  |  |  |  | } | 
| 316 |  |  |  |  |  |  | return 0; | 
| 317 |  |  |  |  |  |  | }; | 
| 318 |  |  |  |  |  |  | if ($latest) { | 
| 319 |  |  |  |  |  |  | if (defined (my $quote_iso = $latest->{'quote_date'})) { | 
| 320 |  |  |  |  |  |  | my $quote_t = $timebase->from_iso_floor ($quote_iso); | 
| 321 |  |  |  |  |  |  | if ($quote_t >= $lo && $quote_t <= $hi) { | 
| 322 |  |  |  |  |  |  | $accumulate->($latest->{'bid'}); | 
| 323 |  |  |  |  |  |  | $accumulate->($latest->{'offer'}); | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  | } | 
| 326 |  |  |  |  |  |  | if (defined (my $last_iso = $latest->{'last_date'})) { | 
| 327 |  |  |  |  |  |  | my $last_t = $timebase->from_iso_floor ($last_iso); | 
| 328 |  |  |  |  |  |  | if ($last_t >= $lo && $last_t <= $hi) { | 
| 329 |  |  |  |  |  |  | $accumulate->($latest->{'last'}); | 
| 330 |  |  |  |  |  |  | $accumulate->($latest->{'high'}); | 
| 331 |  |  |  |  |  |  | $accumulate->($latest->{'low'}); | 
| 332 |  |  |  |  |  |  | } | 
| 333 |  |  |  |  |  |  | } | 
| 334 |  |  |  |  |  |  | } | 
| 335 |  |  |  |  |  |  | for (my $i = $hi; $i >= $lo; $i--) { | 
| 336 |  |  |  |  |  |  | foreach my $value ($values->[$i], $highs->[$i], $lows->[$i]) { | 
| 337 |  |  |  |  |  |  | $accumulate->($value); | 
| 338 |  |  |  |  |  |  | } | 
| 339 |  |  |  |  |  |  | } | 
| 340 |  |  |  |  |  |  |  | 
| 341 |  |  |  |  |  |  | my $extra = ($page - ($h - $l)) / 2; | 
| 342 |  |  |  |  |  |  | $l -= $extra; | 
| 343 |  |  |  |  |  |  | $h += $extra; | 
| 344 |  |  |  |  |  |  | ### initial range decided: "$l $h   $self" | 
| 345 |  |  |  |  |  |  | return ($l, $h); | 
| 346 |  |  |  |  |  |  | } | 
| 347 |  |  |  |  |  |  |  | 
| 348 |  |  |  |  |  |  | # 	    # don't go below `datatype-minimum' (if present), so long as | 
| 349 |  |  |  |  |  |  | # 	    # the actual data respects that minimum | 
| 350 |  |  |  |  |  |  | # 	    (and-let* ((minimum    (datatype-minimum datatype)) | 
| 351 |  |  |  |  |  |  | # 		       (actual-min (apply min-maybe lst-closes)) | 
| 352 |  |  |  |  |  |  | # 		       (           (>= actual-min minimum))) | 
| 353 |  |  |  |  |  |  | # 	      (set! lower (max lower minimum))) | 
| 354 |  |  |  |  |  |  |  | 
| 355 |  |  |  |  |  |  | sub filled_low { | 
| 356 |  |  |  |  |  |  | my ($self) = @_; | 
| 357 |  |  |  |  |  |  | return $self->{'fill_low'}; | 
| 358 |  |  |  |  |  |  | } | 
| 359 |  |  |  |  |  |  | sub filled_high { | 
| 360 |  |  |  |  |  |  | my ($self) = @_; | 
| 361 |  |  |  |  |  |  | return $self->{'fill_high'}; | 
| 362 |  |  |  |  |  |  | } | 
| 363 |  |  |  |  |  |  |  | 
| 364 |  |  |  |  |  |  | sub find_before { | 
| 365 |  |  |  |  |  |  | my ($self, $before, $n) = @_; | 
| 366 |  |  |  |  |  |  | ### Series find_before(): "before=$before n=$n" | 
| 367 |  |  |  |  |  |  | if ($n <= 0) { return $before; } # asking for no points before | 
| 368 |  |  |  |  |  |  |  | 
| 369 |  |  |  |  |  |  | my $values = $self->values_array; | 
| 370 |  |  |  |  |  |  | my $chunk = $n; | 
| 371 |  |  |  |  |  |  |  | 
| 372 |  |  |  |  |  |  | my $i = $before - 1; | 
| 373 |  |  |  |  |  |  | for (;;) { | 
| 374 |  |  |  |  |  |  | $chunk *= 2; | 
| 375 |  |  |  |  |  |  | my $pre = $i - $chunk; | 
| 376 |  |  |  |  |  |  | $self->fill ($pre, $i); | 
| 377 |  |  |  |  |  |  |  | 
| 378 |  |  |  |  |  |  | for ( ; $i >= $pre; $i--) { | 
| 379 |  |  |  |  |  |  | if ($i < 0) { | 
| 380 |  |  |  |  |  |  | ### not found, return 0 | 
| 381 |  |  |  |  |  |  | return 0; | 
| 382 |  |  |  |  |  |  | } | 
| 383 |  |  |  |  |  |  | if (defined $values->[$i]) { | 
| 384 |  |  |  |  |  |  | $n--; | 
| 385 |  |  |  |  |  |  | if ($n <= 0) { | 
| 386 |  |  |  |  |  |  | ### find_before() found: $i | 
| 387 |  |  |  |  |  |  | return $i; | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  | } | 
| 390 |  |  |  |  |  |  | } | 
| 391 |  |  |  |  |  |  | } | 
| 392 |  |  |  |  |  |  | } | 
| 393 |  |  |  |  |  |  |  | 
| 394 |  |  |  |  |  |  | # return pos where there's a value somwhere $pos > $after, or $after if no more | 
| 395 |  |  |  |  |  |  | sub find_after { | 
| 396 |  |  |  |  |  |  | my ($self, $after, $n) = @_; | 
| 397 |  |  |  |  |  |  | ### Series find_after(): "$after n=".($n//'undef') | 
| 398 |  |  |  |  |  |  | if ($n <= 0) { return $after; } # asking for no points after | 
| 399 |  |  |  |  |  |  |  | 
| 400 |  |  |  |  |  |  | my $values = $self->values_array; | 
| 401 |  |  |  |  |  |  | my $hi = $self->hi; | 
| 402 |  |  |  |  |  |  | my $chunk = $n; | 
| 403 |  |  |  |  |  |  |  | 
| 404 |  |  |  |  |  |  | my $i = $after + 1; | 
| 405 |  |  |  |  |  |  | $i = max ($i, 0); | 
| 406 |  |  |  |  |  |  | for (;;) { | 
| 407 |  |  |  |  |  |  | $chunk *= 2; | 
| 408 |  |  |  |  |  |  | my $post = $i + $chunk; | 
| 409 |  |  |  |  |  |  | $self->fill ($i, $post); | 
| 410 |  |  |  |  |  |  | for ( ; $i <= $post; $i++) { | 
| 411 |  |  |  |  |  |  | if ($i > $hi) { return $hi; } | 
| 412 |  |  |  |  |  |  | if (defined $values->[$i]) { | 
| 413 |  |  |  |  |  |  | $n--; | 
| 414 |  |  |  |  |  |  | if ($n <= 0) { | 
| 415 |  |  |  |  |  |  | ### find_after() found: $i | 
| 416 |  |  |  |  |  |  | return $i; | 
| 417 |  |  |  |  |  |  | } | 
| 418 |  |  |  |  |  |  | } | 
| 419 |  |  |  |  |  |  | } | 
| 420 |  |  |  |  |  |  | } | 
| 421 |  |  |  |  |  |  | } | 
| 422 |  |  |  |  |  |  |  | 
| 423 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 424 |  |  |  |  |  |  |  | 
| 425 |  |  |  |  |  |  | sub AUTOLOAD { | 
| 426 |  |  |  |  |  |  | our $AUTOLOAD; | 
| 427 |  |  |  |  |  |  | ### Series AUTOLOAD $AUTOLOAD | 
| 428 |  |  |  |  |  |  | my $name = $AUTOLOAD; | 
| 429 |  |  |  |  |  |  | $name =~ s/(.*):://; | 
| 430 |  |  |  |  |  |  | if (my $subr = __PACKAGE__->can($name)) { | 
| 431 |  |  |  |  |  |  | { no strict; *$name = $subr; } | 
| 432 |  |  |  |  |  |  | goto &$subr; | 
| 433 |  |  |  |  |  |  | } | 
| 434 |  |  |  |  |  |  | croak "App::Chart::Series unknown function '$name'"; | 
| 435 |  |  |  |  |  |  | } | 
| 436 |  |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  | sub can { | 
| 438 |  |  |  |  |  |  | my ($self_or_class, $name) = @_; | 
| 439 |  |  |  |  |  |  | ### Series can(): "$self_or_class '$name'" | 
| 440 |  |  |  |  |  |  |  | 
| 441 |  |  |  |  |  |  | return $self_or_class->SUPER::can($name) || do { | 
| 442 |  |  |  |  |  |  | if ($name =~ /^GT_/p) { | 
| 443 |  |  |  |  |  |  | require App::Chart::Series::GT; | 
| 444 |  |  |  |  |  |  | my $type = "I:${^POSTMATCH}"; | 
| 445 |  |  |  |  |  |  | return sub { App::Chart::Series::GT->new ($type, @_) }; | 
| 446 |  |  |  |  |  |  | } | 
| 447 |  |  |  |  |  |  | if ($name =~ /^TA_/p) { | 
| 448 |  |  |  |  |  |  | require App::Chart::Series::TA; | 
| 449 |  |  |  |  |  |  | my $type = ${^POSTMATCH}; | 
| 450 |  |  |  |  |  |  | return sub { App::Chart::Series::TA->new ($type, @_) }; | 
| 451 |  |  |  |  |  |  | } | 
| 452 |  |  |  |  |  |  | require Module::Util; | 
| 453 |  |  |  |  |  |  | my $class = "App::Chart::Series::Derived::\u$name"; | 
| 454 |  |  |  |  |  |  | Module::Util::find_installed($class) | 
| 455 |  |  |  |  |  |  | || return undef;  # no such plugin | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | #     if (DEBUG) { print "  func $name class $class\n"; | 
| 458 |  |  |  |  |  |  | #                  if (eval { Module::Load::load ($class); 1 }) { | 
| 459 |  |  |  |  |  |  | #                    no strict 'refs'; | 
| 460 |  |  |  |  |  |  | #                    say "    loads ok, new() is ", \&{"${class}::new"}; | 
| 461 |  |  |  |  |  |  | #                  } else { | 
| 462 |  |  |  |  |  |  | #                    say "    didn't load -- $@"; | 
| 463 |  |  |  |  |  |  | #                  } | 
| 464 |  |  |  |  |  |  | #                } | 
| 465 |  |  |  |  |  |  |  | 
| 466 |  |  |  |  |  |  | require Module::Load; | 
| 467 |  |  |  |  |  |  | Module::Load::load ($class); | 
| 468 |  |  |  |  |  |  | return sub { $class->new (@_) }; | 
| 469 |  |  |  |  |  |  | }; | 
| 470 |  |  |  |  |  |  | } | 
| 471 |  |  |  |  |  |  |  | 
| 472 |  |  |  |  |  |  | # avoid going through AUTOLOAD for destroy | 
| 473 |  |  |  |  |  |  | sub DESTROY { | 
| 474 |  |  |  |  |  |  | } | 
| 475 |  |  |  |  |  |  |  | 
| 476 |  |  |  |  |  |  | use overload | 
| 477 |  |  |  |  |  |  | '0+'   => sub { croak 'Cannot use App::Chart::Series as a number' }, | 
| 478 |  |  |  |  |  |  | 'bool' => sub { 1 }, | 
| 479 |  |  |  |  |  |  | '!'    => sub { 0 }, | 
| 480 |  |  |  |  |  |  | '""'   => sub { $_[0] }, | 
| 481 |  |  |  |  |  |  | '@{}'  => sub { $_[0]->fill(0,$_[0]->hi); $_[0]->values_array }, | 
| 482 |  |  |  |  |  |  | '+'   => \&_overload_add, | 
| 483 |  |  |  |  |  |  | '-'   => \&sub, | 
| 484 |  |  |  |  |  |  | '*'   => \&_overload_mul, | 
| 485 |  |  |  |  |  |  | '/'   => \&div, | 
| 486 |  |  |  |  |  |  | 'neg' => \&neg, | 
| 487 |  |  |  |  |  |  | '**'  => \&pow, | 
| 488 |  |  |  |  |  |  | 'abs'  => \&abs, | 
| 489 |  |  |  |  |  |  | 'cos'  => \&cos, | 
| 490 |  |  |  |  |  |  | 'exp'  => \&exp, | 
| 491 |  |  |  |  |  |  | 'int'  => \&int, | 
| 492 |  |  |  |  |  |  | 'log'  => \&log, | 
| 493 |  |  |  |  |  |  | 'sin'  => \&sin, | 
| 494 |  |  |  |  |  |  | 'sqrt' => \&sqrt; | 
| 495 |  |  |  |  |  |  |  | 
| 496 |  |  |  |  |  |  | sub _func { | 
| 497 |  |  |  |  |  |  | my ($series, $subr) = @_; | 
| 498 |  |  |  |  |  |  | require App::Chart::Series::Func; | 
| 499 |  |  |  |  |  |  | return App::Chart::Series::Func->new ($series, $subr); | 
| 500 |  |  |  |  |  |  | } | 
| 501 |  |  |  |  |  |  |  | 
| 502 |  |  |  |  |  |  | sub neg  { $_[0]->_func (sub { - $_[0] }) } | 
| 503 |  |  |  |  |  |  | sub abs  { $_[0]->_func (sub { CORE::abs $_[0] }) } | 
| 504 |  |  |  |  |  |  | sub cos  { $_[0]->_func (sub { CORE::cos $_[0] }) } | 
| 505 |  |  |  |  |  |  | sub exp  { $_[0]->_func (sub { CORE::exp $_[0] }) } | 
| 506 |  |  |  |  |  |  | sub int  { $_[0]->_func (sub { CORE::int $_[0] }) } | 
| 507 |  |  |  |  |  |  | sub log  { $_[0]->_func (sub { CORE::log $_[0] }) } | 
| 508 |  |  |  |  |  |  | sub sin  { $_[0]->_func (sub { CORE::sin $_[0] }) } | 
| 509 |  |  |  |  |  |  | sub sqrt { $_[0]->_func (sub { CORE::sqrt $_[0] }) } | 
| 510 |  |  |  |  |  |  |  | 
| 511 |  |  |  |  |  |  | sub _overload_add { | 
| 512 |  |  |  |  |  |  | my ($x, $y) = @_; | 
| 513 |  |  |  |  |  |  | if (ref $y) { | 
| 514 |  |  |  |  |  |  | # series + series | 
| 515 |  |  |  |  |  |  | require App::Chart::Series::AddSub; | 
| 516 |  |  |  |  |  |  | return App::Chart::Series::AddSub->new ($x, $y); | 
| 517 |  |  |  |  |  |  | } else { | 
| 518 |  |  |  |  |  |  | # series + number | 
| 519 |  |  |  |  |  |  | return $x->_func (sub{ $_[0] + $y }); | 
| 520 |  |  |  |  |  |  | } | 
| 521 |  |  |  |  |  |  | } | 
| 522 |  |  |  |  |  |  | sub sub { | 
| 523 |  |  |  |  |  |  | my ($x, $y, $swap) = @_; | 
| 524 |  |  |  |  |  |  | if (ref $y) { | 
| 525 |  |  |  |  |  |  | # series - series | 
| 526 |  |  |  |  |  |  | require App::Chart::Series::AddSub; | 
| 527 |  |  |  |  |  |  | return App::Chart::Series::AddSub->new (($swap ? ($y, $x) : ($x, $y)), | 
| 528 |  |  |  |  |  |  | negate => 1); | 
| 529 |  |  |  |  |  |  | } else { | 
| 530 |  |  |  |  |  |  | # series - number, or number - series | 
| 531 |  |  |  |  |  |  | return $x->_func ($swap | 
| 532 |  |  |  |  |  |  | ? sub{ $y - $_[0] } | 
| 533 |  |  |  |  |  |  | : sub{ $_[0] - $y }); | 
| 534 |  |  |  |  |  |  | } | 
| 535 |  |  |  |  |  |  | } | 
| 536 |  |  |  |  |  |  | sub _overload_mul { | 
| 537 |  |  |  |  |  |  | my ($x, $y) = @_; | 
| 538 |  |  |  |  |  |  | return $x->mul($y); | 
| 539 |  |  |  |  |  |  | } | 
| 540 |  |  |  |  |  |  | sub div { | 
| 541 |  |  |  |  |  |  | my ($x, $y, $swap) = @_; | 
| 542 |  |  |  |  |  |  | if (ref $y) { croak 'Can only divide a App::Chart::Series by a constant'; } | 
| 543 |  |  |  |  |  |  | if ($swap) { | 
| 544 |  |  |  |  |  |  | croak "Not implemented"; | 
| 545 |  |  |  |  |  |  | } else { | 
| 546 |  |  |  |  |  |  | return $x * (1/$y); | 
| 547 |  |  |  |  |  |  | } | 
| 548 |  |  |  |  |  |  | } | 
| 549 |  |  |  |  |  |  | sub pow { | 
| 550 |  |  |  |  |  |  | my ($series, $power, $swap) = @_; | 
| 551 |  |  |  |  |  |  | if (ref $power) { | 
| 552 |  |  |  |  |  |  | croak __('Can only raise App::Chart::Series to a scalar power'); | 
| 553 |  |  |  |  |  |  | } | 
| 554 |  |  |  |  |  |  | return $series->_func ($swap | 
| 555 |  |  |  |  |  |  | ? sub{ $power ** $_[0] } | 
| 556 |  |  |  |  |  |  | : sub{ $_[0] ** $power }); | 
| 557 |  |  |  |  |  |  | } | 
| 558 |  |  |  |  |  |  |  | 
| 559 |  |  |  |  |  |  | 1; | 
| 560 |  |  |  |  |  |  | __END__ | 
| 561 |  |  |  |  |  |  |  | 
| 562 |  |  |  |  |  |  | =for stopwords undef openint indices undefs delisted autoloads ie | 
| 563 |  |  |  |  |  |  |  | 
| 564 |  |  |  |  |  |  | =head1 NAME | 
| 565 |  |  |  |  |  |  |  | 
| 566 |  |  |  |  |  |  | App::Chart::Series -- series data object | 
| 567 |  |  |  |  |  |  |  | 
| 568 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 569 |  |  |  |  |  |  |  | 
| 570 |  |  |  |  |  |  | use App::Chart::Series; | 
| 571 |  |  |  |  |  |  |  | 
| 572 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 573 |  |  |  |  |  |  |  | 
| 574 |  |  |  |  |  |  | A C<App::Chart::Series> object holds a data series.  It basically holds an | 
| 575 |  |  |  |  |  |  | array or multiple arrays, of values indexed from 0 up to C<< $series->hi >>. | 
| 576 |  |  |  |  |  |  | Portions of the arrays are filled on request with the C<fill> method, so | 
| 577 |  |  |  |  |  |  | just some of a big series can be read from disk or calculated. | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | Array elements with no value for a given date are undef.  The arrays may be | 
| 580 |  |  |  |  |  |  | shorter than C<< $series->hi >> when no data near the end.  And for instance | 
| 581 |  |  |  |  |  |  | the "openint" array of futures open interest is always empty for ordinary | 
| 582 |  |  |  |  |  |  | shares. | 
| 583 |  |  |  |  |  |  |  | 
| 584 |  |  |  |  |  |  | Array indices are values in a timebase, see L<App::Chart::Timebase>, so 0 is | 
| 585 |  |  |  |  |  |  | some starting date, perhaps a particular day, or a whole month or week.  A | 
| 586 |  |  |  |  |  |  | fixed sequence like this with undefs for public holidays or delisted times | 
| 587 |  |  |  |  |  |  | makes it easy to fill portions without knowing how much there might be | 
| 588 |  |  |  |  |  |  | altogether in the database, but skipping undefs all the time when | 
| 589 |  |  |  |  |  |  | calculating is a bit tedious. | 
| 590 |  |  |  |  |  |  |  | 
| 591 |  |  |  |  |  |  | C<App::Chart::Series> itself is just a base class, with various methods | 
| 592 |  |  |  |  |  |  | common to series objects.  Objects are only actually created by subclasses | 
| 593 |  |  |  |  |  |  | such as C<App::Chart::Series::Database>. | 
| 594 |  |  |  |  |  |  |  | 
| 595 |  |  |  |  |  |  | Derived series can be made with autoloads for the derived modules, such as | 
| 596 |  |  |  |  |  |  | C<< $series->SMA(10) >> to calculate a simple moving average.  But maybe the | 
| 597 |  |  |  |  |  |  | way that works will change, since in a chained calculation the full data | 
| 598 |  |  |  |  |  |  | arrays of intermediate parts don't need to be kept, it's just algorithms or | 
| 599 |  |  |  |  |  |  | transformations that need to be combined. | 
| 600 |  |  |  |  |  |  |  | 
| 601 |  |  |  |  |  |  | =head1 FUNCTIONS | 
| 602 |  |  |  |  |  |  |  | 
| 603 |  |  |  |  |  |  | =over 4 | 
| 604 |  |  |  |  |  |  |  | 
| 605 |  |  |  |  |  |  | =item C<< $series->timebase() >> | 
| 606 |  |  |  |  |  |  |  | 
| 607 |  |  |  |  |  |  | Return the C<App::Chart::Timebase> object which is the basis for C<$series> | 
| 608 |  |  |  |  |  |  | (see L<App::Chart::Timebase>). | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | =item C<< $series->decimals() >> | 
| 611 |  |  |  |  |  |  |  | 
| 612 |  |  |  |  |  |  | Return the number of decimal places which should normally be shown for | 
| 613 |  |  |  |  |  |  | values in C<$series>.  For example in a database price series this might be | 
| 614 |  |  |  |  |  |  | 2 to show dollars and cents, but for a series of trading volumes it would be | 
| 615 |  |  |  |  |  |  | 0. | 
| 616 |  |  |  |  |  |  |  | 
| 617 |  |  |  |  |  |  | This is only an intended accuracy to display (or minimum accuracy), not a | 
| 618 |  |  |  |  |  |  | limit on the accuracy of the values in C<$series>. | 
| 619 |  |  |  |  |  |  |  | 
| 620 |  |  |  |  |  |  | =item C<< $series->symbol() >> | 
| 621 |  |  |  |  |  |  |  | 
| 622 |  |  |  |  |  |  | Return the stock or commodity symbol for the data in this series, or | 
| 623 |  |  |  |  |  |  | C<undef> if it's not associated with a symbol at all. | 
| 624 |  |  |  |  |  |  |  | 
| 625 |  |  |  |  |  |  | =item C<< $series->hi() >> | 
| 626 |  |  |  |  |  |  |  | 
| 627 |  |  |  |  |  |  | Return the maximum index into the series arrays, ie. the arrays can be | 
| 628 |  |  |  |  |  |  | filled with data from 0 up to C<< $series->hi >> inclusive. | 
| 629 |  |  |  |  |  |  |  | 
| 630 |  |  |  |  |  |  | =item C<< $series->fill ($lo, $hi) >> | 
| 631 |  |  |  |  |  |  |  | 
| 632 |  |  |  |  |  |  | Ask for data to be available for the arrays from C<$lo> to C<$hi> inclusive. | 
| 633 |  |  |  |  |  |  | This might read the database, or make a data calculation, etc, if it hasn't | 
| 634 |  |  |  |  |  |  | already been done for the range. | 
| 635 |  |  |  |  |  |  |  | 
| 636 |  |  |  |  |  |  | If C<$lo> or C<$hi> are outside the actual available range (ie. C<$lo> | 
| 637 |  |  |  |  |  |  | negative and/or C<$hi> above C<< $series->hi >>), then just the actual | 
| 638 |  |  |  |  |  |  | available parts are loaded and the excess ignored. | 
| 639 |  |  |  |  |  |  |  | 
| 640 |  |  |  |  |  |  | =item C<< $series->range ($lo, $hi) >> | 
| 641 |  |  |  |  |  |  |  | 
| 642 |  |  |  |  |  |  | Return two values C<($lower, $upper)> which is the range of values taken by | 
| 643 |  |  |  |  |  |  | C<$series> between timebase values C<$lo> and C<$hi>, inclusive.  If there's | 
| 644 |  |  |  |  |  |  | no data at all in that range the return is an empty list C<()>. | 
| 645 |  |  |  |  |  |  |  | 
| 646 |  |  |  |  |  |  | =item C<< $series->initial_range ($lo, $hi) >> | 
| 647 |  |  |  |  |  |  |  | 
| 648 |  |  |  |  |  |  | Return two values C<($lower, $upper)> which is a good price range | 
| 649 |  |  |  |  |  |  | (vertically) to display for the data between points C<$lo> and C<$hi>.  If | 
| 650 |  |  |  |  |  |  | there's no data in that range the return is an empty list C<()>. | 
| 651 |  |  |  |  |  |  |  | 
| 652 |  |  |  |  |  |  | =back | 
| 653 |  |  |  |  |  |  |  | 
| 654 |  |  |  |  |  |  | =head1 HOME PAGE | 
| 655 |  |  |  |  |  |  |  | 
| 656 |  |  |  |  |  |  | L<http://user42.tuxfamily.org/chart/index.html> | 
| 657 |  |  |  |  |  |  |  | 
| 658 |  |  |  |  |  |  | =head1 LICENCE | 
| 659 |  |  |  |  |  |  |  | 
| 660 |  |  |  |  |  |  | Copyright 2007, 2008, 2009, 2010, 2011, 2016 Kevin Ryde | 
| 661 |  |  |  |  |  |  |  | 
| 662 |  |  |  |  |  |  | Chart is free software; you can redistribute it and/or modify it under the | 
| 663 |  |  |  |  |  |  | terms of the GNU General Public License as published by the Free Software | 
| 664 |  |  |  |  |  |  | Foundation; either version 3, or (at your option) any later version. | 
| 665 |  |  |  |  |  |  |  | 
| 666 |  |  |  |  |  |  | Chart is distributed in the hope that it will be useful, but WITHOUT ANY | 
| 667 |  |  |  |  |  |  | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 
| 668 |  |  |  |  |  |  | FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more | 
| 669 |  |  |  |  |  |  | details. | 
| 670 |  |  |  |  |  |  |  | 
| 671 |  |  |  |  |  |  | You should have received a copy of the GNU General Public License along with | 
| 672 |  |  |  |  |  |  | Chart; see the file F<COPYING>.  Failing that, see | 
| 673 |  |  |  |  |  |  | L<http://www.gnu.org/licenses/>. | 
| 674 |  |  |  |  |  |  |  | 
| 675 |  |  |  |  |  |  | =cut |