File Coverage

blib/lib/Finance/GeniusTrader/Prices.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Finance::GeniusTrader::Prices;
2              
3             # Copyright 2000-2002 Raphaël Hertzog, Fabien Fulhaber
4             # This file is distributed under the terms of the General Public License
5             # version 2 or (at your option) any later version.
6              
7 1     1   6 use strict;
  1         3  
  1         42  
8 1     1   5 use vars qw(@ISA @EXPORT $FIRST $OPEN $HIGH $LOW $CLOSE $LAST $VOLUME $DATE);
  1         2  
  1         105  
9              
10 1     1   565 use Date::Calc qw(Decode_Date_US Decode_Date_EU Today);
  0            
  0            
11             #ALL# use Log::Log4perl qw(:easy);
12             use Finance::GeniusTrader::DateTime;
13              
14             require Exporter;
15             @ISA = qw(Exporter);
16             @EXPORT = qw($FIRST $OPEN $HIGH $LOW $LAST $CLOSE $VOLUME $DATE);
17              
18             $FIRST = $OPEN = 0;
19             $HIGH = 1;
20             $LOW = 2;
21             $LAST = $CLOSE = 3;
22             $VOLUME = 4;
23             $DATE = 5;
24              
25             =head1 NAME
26              
27             Finance::GeniusTrader::Prices - A serie of prices
28              
29             =head1 DESCRIPTION
30              
31             Finance::GeniusTrader::Prices stores all historic prices (open, high, low, close, volume, date).
32              
33             =over
34              
35             =item C<< my $p = Finance::GeniusTrader::Prices->new() >>
36              
37             Create an empty Finance::GeniusTrader::Prices object.
38              
39             =cut
40             sub new {
41             my $type = shift;
42             my $class = ref($type) || $type;
43              
44             my $self = { 'prices' => [], 'has_date' => '' };
45             return bless $self, $class;
46             }
47              
48             =item C<< $p->at(i) >>
49              
50             Get the prices of the corresponding day. The indice can be obtained
51             from the dates by using $q->date('YYYY-MM-DD').
52              
53             =item C<< $p->at_date('YYYY-MM-DD') >>
54              
55             Get the prices of the corresponding date.
56              
57             =cut
58             sub at {
59             my ($self, $i) = @_;
60             return $self->{'prices'}[$i];
61             }
62             sub at_date {
63             my ($self, $date) = @_;
64             return $self->at($self->date($date));
65             }
66              
67             =item C<< $p->has_date('YYYY-MM-DD') >>
68              
69             Return true if the object has prices for the corresponding date.
70              
71             NOTE: If we test for an item that is larger than the last entry in the
72             prices array, then a new empty entry is created (and numerous error messages
73             as well).
74              
75             =cut
76             sub has_date {
77             my ($self, $date) = @_;
78             my $value = _binary_search($self->{'prices'}, $date);
79             if (defined($value)) {
80             #Often, a call to has_date preceds a call to date
81             #so we might as well cache this value, so we won't need to call
82             #the _binary_search function twice
83             $self->{'has_date'} = $date;
84             $self->{'date_pos'} = $value;
85             return 1;
86             }
87             return 0;
88             }
89              
90             =item C<< $p->date('YYYY-MM-DD') >>
91              
92             Get the indice corresponding to the date 'YYYY-MM-DD'.
93              
94             =cut
95             sub date {
96             my ($self, $date) = @_;
97             return $self->{'date_pos'} if ($self->{'has_date'} eq $date);
98             return _binary_search($self->{'prices'}, $date);
99             }
100              
101             =item C<< $p->add_prices_array([@price_array]) >>
102              
103             =cut
104             sub add_prices_array {
105             my ($self, @prices) = @_;
106             push @{$self->{'prices'}}, @prices;
107             }
108              
109             =item C<< $p->add_prices([$open, $high, $low, $close, $volume, $date]) >>
110              
111             =cut
112             sub add_prices {
113             my ($self, $prices) = @_;
114             push @{$self->{'prices'}}, $prices;
115             }
116              
117             =item C<< $p->count() >>
118              
119             Get the number of prices availables.
120              
121             =cut
122             sub count {
123             return scalar(@{shift->{'prices'}});
124             }
125              
126             =item C<< $p->set_timeframe($timeframe) >>
127              
128             =item C<< $p->timeframe() >>
129              
130             Defines the time frame used for the prices. It's one of the value exported
131             by Finance::GeniusTrader::DateTime;
132              
133             =cut
134             sub set_timeframe { $_[0]->{'timeframe'} = $_[1] }
135             sub timeframe { return $_[0]->{'timeframe'} }
136              
137             =item C<< $p->sort() >>
138              
139             Sort the prices by date.
140              
141             =cut
142             sub sort {
143             my ($self) = @_;
144             my @prices = sort {
145             Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $a->[$DATE]) <=>
146             Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $b->[$DATE])
147             } @{$self->{'prices'}};
148             $self->{'prices'} = \@prices;
149             }
150              
151             =item C<< $p->reverse() >>
152              
153             Reverse the prices list.
154              
155             =cut
156             sub reverse {
157             my ($self) = @_;
158             my @prices = reverse @{$self->{'prices'}};
159             $self->{'prices'} = \@prices;
160             }
161              
162             =item C<< $p->convert_to_timeframe($timeframe) >>
163              
164             Creates a new Prices object using the new timeframe by merging the
165             required prices. You can only convert to a largest timeframe.
166              
167             =cut
168             sub convert_to_timeframe {
169             my ($self, $timeframe) = @_;
170              
171             #WAR# WARN "new timeframe must be larger" unless ($timeframe > $self->timeframe);
172             my $prices = Finance::GeniusTrader::Prices->new($self->count);
173             $prices->set_timeframe($timeframe);
174              
175             # Initialize the iteration
176             my ($open, $high, $low, $close, $volume, $date) = @{$self->{'prices'}[0]};
177             $volume = 0;
178             my ($prevdate, $newdate);
179             $prevdate = Finance::GeniusTrader::DateTime::convert_date($date, $self->timeframe, $timeframe);
180              
181             # Iterate over all the prices (hope they are sorted)
182             foreach my $q (@{$self->{'prices'}})
183             {
184             # Build the date in the new timeframe corresponding to the prices
185             # being treated
186             $newdate = Finance::GeniusTrader::DateTime::convert_date($q->[$DATE], $self->timeframe,
187             $timeframe);
188             # If the date differs from the previous one then we have completed
189             # a new item
190             if ($newdate ne $prevdate) {
191             # Store the new item
192             $prices->add_prices([ $open, $high, $low, $close, $volume,
193             $prevdate ]);
194             # Initialize the open/high/low/close with the following item
195             $open = $q->[$OPEN];
196             $high = $q->[$HIGH];
197             $low = $q->[$LOW];
198             $close = $q->[$CLOSE];
199             $volume = 0;
200             }
201             # Update the data of the item that is being built
202             $high = ($q->[$HIGH] > $high) ? $q->[$HIGH] : $high;
203             $low = ($q->[$LOW] < $low) ? $q->[$LOW] : $low;
204             $close = $q->[$CLOSE];
205             $volume += $q->[$VOLUME];
206              
207             # Update the previous date
208             $prevdate = $newdate;
209             }
210             # Store the last item
211             $prices->add_prices([ $open, $high, $low, $close, $volume, $prevdate ]);
212              
213             return $prices;
214             }
215              
216             =item C<< $p->find_nearest_following_date($date) >>
217              
218             =item C<< $p->find_nearest_preceding_date($date) >>
219              
220             =item C<< $p->find_nearest_date($date) >>
221              
222             Find the nearest date available
223              
224             =cut
225             sub find_nearest_following_date {
226             my ($self, $date) = @_;
227             my $time = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $date);
228             my $mindiff = $time;
229             my $mindate = '';
230             foreach (@{$self->{'prices'}})
231             {
232             my $dtime = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $_->[$DATE]);
233             my $diff = $dtime - $time;
234             next if ($diff < 0);
235             if ($diff < $mindiff)
236             {
237             $mindate = $_->[$DATE];
238             $mindiff = $diff;
239             }
240             }
241             return $mindate;
242             }
243              
244             sub find_nearest_preceding_date {
245             my ($self, $date) = @_;
246             my $time = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $date);
247             my $mindiff = $time;
248             my $mindate = '';
249             foreach (@{$self->{'prices'}})
250             {
251             my $dtime = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $_->[$DATE]);
252             my $diff = $time - $dtime;
253             next if ($diff < 0);
254             if ($diff < $mindiff)
255             {
256             $mindate = $_->[$DATE];
257             $mindiff = $diff;
258             }
259             }
260             return $mindate;
261             }
262              
263             sub find_nearest_date {
264             my ($self, $date) = @_;
265             my $time = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $date);
266             my $mindiff = $time;
267             my $mindate = '';
268             foreach (@{$self->{'prices'}})
269             {
270             my $dtime = Finance::GeniusTrader::DateTime::map_date_to_time($self->timeframe, $_->[$DATE]);
271             my $diff = abs($time - $dtime);
272             if ($diff < $mindiff)
273             {
274             $mindate = $_->[$DATE];
275             $mindiff = $diff;
276             }
277             }
278             return $mindate;
279             }
280              
281             =item C<< $p->loadtxt("cotationsfile.txt") >>
282              
283             Load the prices from the text file.
284              
285             =cut
286             sub loadtxt {
287             my ($self, $file, $mark, $date_format, $skip, %fields) = @_;
288              
289             open(FILE, '<', "$file") || die "Can't open $file: $!\n";
290             # unless(open(FILE, '<', "$file")} || (warn "Can't open $file: $!\n" and return;
291              
292             $self->{'prices'} = [];
293             my ($open, $high, $low, $close, $volume, $date);
294             my ($year, $month, $day, $tm);
295              
296             # Initialize all options with the default settings
297             # Set up $mark as a tabulation
298             if (!$mark) { $mark = "\t"; }
299              
300             # Set up %fields with the standard fields map : open high low close volume date
301             if (!%fields) {
302             %fields = ('open' => 0, 'high' => 1, 'low' => 2, 'close' => 3, 'volume' => 4, 'date' => 5);
303             }
304            
305             # Set up $date_format to the US date format
306             if (!$date_format) { $date_format = 0; }
307            
308             # Process each line in $file...
309             while (defined($_=))
310             {
311             # Skip user specified number of file header lines
312             if ( $skip > 0 ) {
313             $skip--;
314             next;
315             }
316            
317             # ... only if it's a line without strings (ie: everything but head line)
318             next if (/^[#<]/); #Skip comments and METASTOCK ascii file header
319             #next if (/\G[A-Za-z]/gc); #Skip all lines containing text strings
320             #NOTE: The first does not skip typical headers; the second does
321             # not allow textual dates.
322              
323             if (!/date/ig) {
324              
325             # Get and split the line with $mark
326             chomp;
327             my @line = split("$mark");
328              
329             # Get and swap all necessary fields according to the fields map
330             $open = $line[$fields{'open'}];
331             $high = $line[$fields{'high'}];
332             $low = $line[$fields{'low'}];
333             $close = $line[$fields{'close'}];
334             $volume = $line[$fields{'volume'}] || 0;
335             my @datetime_fields = split(',',$fields{'date'});
336             my $datetime_fields_count = scalar(@datetime_fields);
337             my $date=$line[$datetime_fields[0]];
338             for (my $i=1; $i<$datetime_fields_count;$i++) {
339             $date .= ' '.$line[$datetime_fields[$i]];
340             }
341              
342             # Decode the date from the text file to something useable
343             # The hh:nn:ss part is optional
344             # $date_format eq 0 : GeniusTrader Date Format (yyyy-mm-dd hh:nn:ss)
345             # $date_format eq 1 : US sort of Date Format (month before day)
346             # $date_format eq 2 : EU sort of Date Format (day before month)
347             # $date_format eq 3 : Any format understood by Date::Manip
348            
349             if ($date_format != 0) {
350            
351             if ($date_format eq 1) {
352             ($year, $month, $day) = Decode_Date_US($date);
353             }
354             if ($date_format eq 2) {
355             ($year, $month, $day) = Decode_Date_EU($date);
356             }
357             if ($date_format eq 3) {
358             use Date::Manip;
359             #Date::Manip requires this to be defined
360             #there probably is a better way of doing this
361             #rather than defining it here, but it works
362             #for now
363             $ENV{'TZ'} = 'GMT' unless(defined($ENV{'TZ'}));
364             my $udate = &UnixDate($date, '%Y-%m-%d %H:%M:%S');
365             unless (defined $udate) {
366             warn "Incorrect date for format $date_format: $date.\n";
367             next;
368             }
369             ( $year, $month, $day, $tm ) = split /[- ]/, $udate;
370             }
371             unless (defined $year) {
372             warn "Incorrect date for format $date_format: $date.\n";
373             next;
374             }
375             my ($today_year, $today_month, $today_day) = Today();
376             if ($year > $today_year) {
377             $year -= 100;
378             }
379             # Time::Local only works for dates within 50 years
380             next if $year <= $today_year - 50;
381             unless ($date_format eq 3) {
382             $month = '0' . $month if $month < 10;
383             $day = '0' . $day if $day < 10;
384             }
385             $date = $year . '-' . $month . '-' .$day;
386             $date .= " $tm" if $tm;
387             }
388              
389             # Add all data within the Finance::GeniusTrader::Prices object
390             $self->add_prices([ $open, $high, $low, $close, $volume, $date ]);
391             }
392             }
393             close FILE;
394             }
395              
396             =item C<< $p->savetxt("cotationsfile.txt") >>
397              
398             Save the prices to the text file.
399              
400             =cut
401             sub savetxt {
402             my ($self, $file, $mark, $date_format, %fields) = @_;
403             open(FILE, '>', "$file") || die "Can't write in $file: $!\n";
404             my ($open, $high, $low, $close, $volume, $date);
405             my ($year, $month, $day);
406              
407             $mark = $mark || "\t";
408              
409             # Set up %fields with the standard fields map : open high low close volume date
410             if (!%fields) {
411             %fields = ('open' => 0, 'high' => 1, 'low' => 2, 'close' => 3, 'volume' => 4, 'date' => 5);
412             }
413            
414             # Set up $date_format to the internal date format
415             if (!$date_format) { $date_format = 0; }
416            
417             foreach (@{$self->{'prices'}})
418             {
419             my @line = @{$_};
420             $open = $line[$fields{'open'}];
421             $high = $line[$fields{'high'}];
422             $low = $line[$fields{'low'}];
423             $close = $line[$fields{'close'}];
424             $volume = $line[$fields{'volume'}];
425             $date = $line[$fields{'date'}]; # No obvious way how to divide date into mulitple fields
426              
427             if ($date_format != 0) {
428            
429             if ($date_format eq 1) {
430             ($year, $month, $day) = split(/-/, $date);
431             $date = "$month/$day/$year";
432             }
433             if ($date_format eq 2) {
434             ($year, $month, $day) = split(/-/, $date);
435             $date = "$day/$month/$year";
436             }
437             }
438              
439             my @newline = ();
440             $newline[$fields{'open'}] = $open;
441             $newline[$fields{'high'}] = $high;
442             $newline[$fields{'low'}] = $low;
443             $newline[$fields{'close'}] = $close;
444             $newline[$fields{'volume'}] = $volume;
445             $newline[$fields{'date'}] = $date;
446            
447             print FILE join($mark, @newline) . "\n";
448             }
449             close FILE;
450             }
451              
452             =item C<< $p->dump; >>
453              
454             Print the prices on the standard output.
455              
456             =cut
457             sub dump {
458             my ($self, $mark) = @_;
459             $mark = $mark || "\t";
460             foreach (@{$self->{'prices'}})
461             {
462             print join($mark, @{$_}) . "\n";
463             }
464             }
465              
466             ## PRIVATE FUNCTIONS
467              
468             =item C<< $p->_binary_search($array_ref, $value) >>
469              
470             Searches for the given $value in the $DATE position of the prices array.
471             This is an internal function, meant to be used only inside this object.
472              
473             =cut
474             sub _binary_search {
475             my ($array_ref, $value) = @_;
476             my ($first, $last) = (0, scalar(@$array_ref)-1);
477              
478             while ($first <= $last) {
479             my $middle = int(($first + $last) / 2);
480             if ($$array_ref[$middle][$DATE] eq $value) {
481             return $middle;
482             } elsif ($$array_ref[$middle][$DATE] lt $value) {
483             $first = $middle + 1;
484             } else {
485             $last = $middle - 1;
486             }
487             }
488             return undef;
489             }
490              
491              
492             =pod
493              
494             =back
495              
496             =cut
497             1;