File Coverage

blib/lib/Finance/Calendar.pm
Criterion Covered Total %
statement 287 300 95.6
branch 101 130 77.6
condition 47 84 55.9
subroutine 42 42 100.0
pod 23 23 100.0
total 500 579 86.3


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Finance::Calendar - represents the trading calendar.
5              
6             =head1 SYNOPSIS
7              
8             use Finance::Calendar;
9             use Date::Utility;
10              
11             my $calendar = {
12             holidays => {
13             "25-Dec-2013" => {
14             "Christmas Day" => [qw(FOREX METAL)],
15             },
16             "1-Jan-2014" => {
17             "New Year's Day" => [qw( FOREX METAL)],
18             },
19             "1-Apr-2013" => {
20             "Easter Monday" => [qw( USD)],
21             },
22             },
23             early_closes => {
24             '24-Dec-2009' => {
25             '16:30' => ['HKSE'],
26             },
27             '22-Dec-2016' => {
28             '18:00' => ['FOREX', 'METAL'],
29             },
30             },
31             late_opens => {
32             '24-Dec-2010' => {
33             '14:30' => ['HKSE'],
34             },
35             },
36             };
37             my $calendar = Finance::Calendar->new(calendar => $calendar);
38             my $now = Date::Utility->new;
39              
40             # Does London Stocks Exchange trade on $now
41             $calendar->trades_on(Finance::Exchange->create_exchange('LSE'), $now);
42              
43             # Is it a country holiday for the United States on $now
44             $calendar->is_holiday_for('USD', $now);
45              
46             # Returns the opening time of Australian Stocks Exchange on $now
47             $calendar->opening_on(Finance::Exchange->create_exchange('ASX'), $now);
48              
49             # Returns the closing time of Forex on $now
50             $calendar->closing_on(Finance::Exchange->create_exchange('FOREX'), $now);
51             ...
52              
53             =head1 DESCRIPTION
54              
55             This class is responsible for providing trading times or holidays related information of a given financial stock exchange on a specific date.
56              
57             =cut
58              
59             use Moose;
60 1     1   640724  
  1         11  
  1         7  
61             our $VERSION = '0.03';
62              
63             use List::Util qw(min max first);
64 1     1   5936 use Date::Utility;
  1         2  
  1         69  
65 1     1   523 use Memoize;
  1         625786  
  1         79  
66 1     1   665 use Finance::Exchange;
  1         2133  
  1         45  
67 1     1   6 use Carp qw(croak);
  1         3  
  1         25  
68 1     1   5  
  1         2  
  1         3580  
69             =head1 ATTRIBUTES - Object Construction
70              
71             =head2 calendar
72              
73             A hash reference that has information on:
74             - exchange and country holidays
75             - late opens
76             - early closes
77              
78             =cut
79              
80             has calendar => (
81             is => 'ro',
82             required => 1,
83             );
84              
85             has _cache => (
86             is => 'ro',
87             default => sub { {} },
88             );
89              
90             my ($self, $method_name, $exchange, @dates) = @_;
91              
92 447     447   746 return undef unless exists $self->_cache->{$method_name};
93              
94 447 100       9501 my $key = join "_", ($exchange->symbol, (map { $self->trading_date_for($exchange, $_)->epoch } @dates));
95             return $self->_cache->{$method_name}{$key};
96 439         8184 }
  443         2757  
97 439         25963  
98             my ($self, $value, $method_name, $exchange, @dates) = @_;
99              
100             my $key = join "_", ($exchange->symbol, (map { $self->trading_date_for($exchange, $_)->epoch } @dates));
101 177     177   350 $self->_cache->{$method_name}{$key} = $value;
102              
103 177         3350 return undef;
  183         1227  
104 177         9809 }
105              
106 177         287 =head1 METHODS - TRADING DAYS RELATED
107              
108             =head2 trades_on
109              
110             ->trades_on($exchange_object, $date_object);
111              
112             Returns true if trading is done on the day of a given Date::Utility.
113              
114             =cut
115              
116             my ($self, $exchange, $when) = @_;
117              
118             if (my $cache = $self->_get_cache('trades_on', $exchange, $when)) {
119             return $cache;
120 267     267 1 3438 }
121              
122 267 100       455 my $really_when = $self->trading_date_for($exchange, $when);
123 171         465 my $result = (@{$exchange->trading_days_list}[$really_when->day_of_week] && !$self->is_holiday_for($exchange->symbol, $really_when)) ? 1 : 0;
124              
125             $self->_set_cache($result, 'trades_on', $exchange, $when);
126 96         193 return $result;
127 96 100 100     3742 }
128              
129 96         541 =head2 trade_date_before
130 96         241  
131             ->trade_date_before($exchange_object, $date_object);
132              
133             Returns a Date::Utility object for the previous trading day of an exchange for the given date.
134              
135             =cut
136              
137             my ($self, $exchange, $when) = @_;
138              
139             my $begin = $self->trading_date_for($exchange, $when);
140              
141             if (my $cache = $self->_get_cache('trade_date_before', $exchange, $begin)) {
142 5     5 1 6690 return $cache;
143             }
144 5         13  
145             my $date_behind;
146 5 50       79 my $counter = 1;
147 0         0  
148             # look back at most 10 days. The previous trading day could have span over a weekend with multiple consecutive holidays.
149             # Previously it was 7 days, but need to increase a little bit since there is a case
150 5         8 # where the holidays was more than 7. That was during end of ramadhan at Saudi Arabia Exchange.
151 5         6 while (not $date_behind and $counter < 10) {
152             my $possible = $begin->minus_time_interval($counter . 'd');
153             $date_behind = $possible if $self->trades_on($exchange, $possible);
154             $counter++;
155             }
156 5   66     18  
157 8         25 $self->_set_cache($date_behind, 'trade_date_before', $exchange, $begin);
158 8 100       516 return $date_behind;
159 8         25 }
160              
161             =head2 trade_date_after
162 5         13  
163 5         92 ->trade_date_after($exchange_object, $date_object);
164              
165             Returns a Date::Utility object of the next trading day of an exchange for a given date.
166              
167             =cut
168              
169             my ($self, $exchange, $date) = @_;
170              
171             my $date_next;
172             my $counter = 1;
173             my $begin = $self->trading_date_for($exchange, $date);
174              
175 15     15 1 7638 if (my $cache = $self->_get_cache('trade_date_after', $exchange, $begin)) {
176             return $cache;
177 15         22 }
178 15         20  
179 15         31 # look forward at most 11 days. The next trading day could have span over a weekend with multiple consecutive holidays.
180             # We chosed 11 due to the fact that the longest trading holidays we have got so far was 10 days(TSE).
181 15 100       512 while (not $date_next and $counter <= 11) {
182 2         5 my $possible = $begin->plus_time_interval($counter . 'd');
183             $date_next = $possible if $self->trades_on($exchange, $possible);
184             $counter++;
185             }
186              
187 13   66     52 $self->_set_cache($date_next, 'trade_date_after', $exchange, $begin);
188 22         71 return $date_next;
189 22 100       3026 }
190 22         67  
191             =head2 trading_date_for
192              
193 13         31 ->trading_date_for($exchange_object, $date_object);
194 13         121  
195             The date on which trading is considered to be taking place even if it is not the same as the GMT date.
196             Note that this does not handle trading dates are offset forward beyond the next day (24h). It will need additional work if these are found to exist.
197              
198             Returns a Date object representing midnight GMT of the trading date.
199              
200             =cut
201              
202             my ($self, $exchange, $date) = @_;
203              
204             # if there's no pre-midnight open, then returns the same day.
205             return $date->truncate_to_day unless ($exchange->trading_date_can_differ);
206              
207             my $next_day = $date->plus_time_interval('1d')->truncate_to_day;
208             my $open_ti =
209 857     857 1 3559 $exchange->market_times->{$self->_times_dst_key($exchange, $next_day)}->{daily_open};
210              
211             return $next_day if ($open_ti and $next_day->epoch + $open_ti->seconds <= $date->epoch);
212 857 100       17337 return $date->truncate_to_day;
213             }
214 35         992  
215             =head2 calendar_days_to_trade_date_after
216 35         4354  
217             ->calendar_days_to_trade_date_after($exchange_object, $date_object);
218 35 100 66     2409  
219 34         1284 Returns the number of calendar days between a given Date::Utility
220             and the next day on which trading is open.
221              
222             =cut
223              
224             my ($self, $exchange, $when) = @_;
225              
226             if (my $cache = $self->_get_cache('calendar_days_to_trade_date_after', $exchange, $when)) {
227             return $cache;
228             }
229              
230             my $number_of_days = $self->trade_date_after($exchange, $when)->days_between($when);
231              
232 6     6 1 4063 $self->_set_cache($number_of_days, 'calendar_days_to_trade_date_after', $exchange, $when);
233             return $number_of_days;
234 6 50       15 }
235 0         0  
236             =head2 trading_days_between
237              
238 6         15  
239             ->trading_days_between($exchange_object, Date::Utility->new('4-May-10'),Date::Utility->new('5-May-10'));
240 6         458  
241 6         27 Returns the number of trading days _between_ two given dates.
242              
243             =cut
244              
245             my ($self, $exchange, $begin, $end) = @_;
246              
247             if (my $cache = $self->_get_cache('trading_days_between', $exchange, $begin, $end)) {
248             return $cache;
249             }
250              
251             # Count up how many are trading days.
252             my $number_of_days = scalar grep { $self->trades_on($exchange, $_) } @{$self->_days_between($begin, $end)};
253              
254 4     4 1 4493 $self->_set_cache($number_of_days, 'trading_days_between', $exchange, $begin, $end);
255             return $number_of_days;
256 4 50       10 }
257 0         0  
258             =head2 holiday_days_between
259              
260             ->holiday_days_between($exchange_object, Date::Utility->new('4-May-10'),Date::Utility->new('5-May-10'));
261 4         8  
  6         35  
  4         54  
262             Returns the number of holidays _between_ two given dates.
263 4         61  
264 4         15 =cut
265              
266             my ($self, $exchange, $begin, $end) = @_;
267              
268             if (my $cache = $self->_get_cache('holiday_days_between', $exchange, $begin, $end)) {
269             return $cache;
270             }
271              
272             # Count up how many are trading days.
273             my $number_of_days = scalar grep { $self->is_holiday_for($exchange->symbol, $_) } @{$self->_days_between($begin, $end)};
274              
275             $self->_set_cache($number_of_days, 'holiday_days_between', $exchange, $begin, $end);
276 2     2 1 3678 return $number_of_days;
277             }
278 2 50       8  
279 0         0 =head1 METHODS - TRADING TIMES RELATED.
280              
281             =head2 is_open
282              
283 2         4 ->is_open($exchange_object);
  11         271  
  2         28  
284              
285 2         6 Returns true is exchange is open now, false otherwise.
286 2         9  
287             =cut
288              
289             my ($self, $exchange) = @_;
290              
291             return $self->is_open_at($exchange, Date::Utility->new);
292             }
293              
294             =head2 is_open_at
295              
296             ->is_open_at($exchange_object, $epoch);
297              
298             Return true is exchange is open at the given epoch, false otherwise.
299              
300 1     1 1 10227 =cut
301              
302 1         9 my ($self, $exchange, $date) = @_;
303             my $opening = $self->opening_on($exchange, $date);
304             return undef if (not $opening or $self->_is_in_trading_break($exchange, $date));
305             return 1 if (not $date->is_before($opening) and not $date->is_after($self->closing_on($exchange, $date)));
306             # if everything falls through, assume it is not open
307             return undef;
308             }
309              
310             =head2 seconds_since_open_at
311              
312             ->seconds_since_open_at($exchange_object, $epoch);
313              
314 10     10 1 5196 Returns the number of seconds since the exchange opened from the given epoch.
315 10         24  
316 10 100 100     43 =cut
317 8 100 100     21  
318             my ($self, $exchange, $date) = @_;
319 5         48  
320             return $self->_market_opens($exchange, $date)->{'opened'};
321             }
322              
323             =head2 seconds_since_close_at
324              
325             ->seconds_since_close_at($exchange_object, $epoch);
326              
327             Returns the number of seconds since the exchange closed from the given epoch.
328              
329             =cut
330              
331 5     5 1 239 my ($self, $exchange, $date) = @_;
332              
333 5         16 return $self->_market_opens($exchange, $date)->{'closed'};
334             }
335              
336             =head2 opening_on
337              
338             ->opening_on($exchange_object, Date::Utility->new('25-Dec-10')); # returns undef (given Xmas is a holiday)
339              
340             Returns the opening time (Date::Utility) of the exchange for a given Date::Utility, undefined otherwise.
341              
342             =cut
343              
344             my ($self, $exchange, $when) = @_;
345 5     5 1 2890  
346             if (my $cache = $self->_get_cache('opening_on', $exchange, $when)) {
347 5         15 return $cache;
348             }
349              
350             my $opening_on = $self->opens_late_on($exchange, $when) // $self->get_exchange_open_times($exchange, $when, 'daily_open');
351              
352             $self->_set_cache($opening_on, 'opening_on', $exchange, $when);
353             return $opening_on;
354             }
355              
356             =head2 closing_on
357              
358             ->closing_on($exchange_object, Date::Utility->new('25-Dec-10')); # returns undef (given Xmas is a holiday)
359 81     81 1 3088  
360             Returns the closing time (Date::Utility) of the exchange for a given Date::Utility, undefined otherwise.
361 81 100       151  
362 51         692 =cut
363              
364             my ($self, $exchange, $when) = @_;
365 30   100     74  
366             if (my $cache = $self->_get_cache('closing_on', $exchange, $when)) {
367 30         76 return $cache;
368 30         180 }
369              
370             my $closing_on = $self->closes_early_on($exchange, $when) // $self->get_exchange_open_times($exchange, $when, 'daily_close');
371              
372             $self->_set_cache($closing_on, 'closing_on', $exchange, $when);
373             return $closing_on;
374             }
375              
376             =head2 trading_breaks
377              
378             ->trading_breaks($exchange_object, $date_object);
379              
380 67     67 1 3512 Defines the breaktime for this exchange.
381              
382 67 100       108 =cut
383 46         674  
384             my ($self, $exchange, $when) = @_;
385              
386 21   100     53 return $self->get_exchange_open_times($exchange, $when, 'trading_breaks');
387             }
388 21         55  
389 21         190 =head2 regularly_adjusts_trading_hours_on
390              
391             Returns a hashref of special-case changes that may apply on specific
392             trading days. Currently, this applies on Fridays only:
393              
394             =over 4
395              
396              
397             =item * for forex or metals
398              
399             =back
400              
401 64     64 1 1778 Example:
402              
403 64         199 $calendar->regularly_adjusts_trading_hours_on('FOREX', time);
404              
405             =cut
406              
407             my ($self, $exchange, $when) = @_;
408              
409             # Only applies on Fridays
410             return undef if $when->day_of_week != 5;
411              
412             my $changes;
413              
414             my $rule = 'Fridays';
415             if ($exchange->symbol eq 'FOREX' or $exchange->symbol eq 'METAL') {
416             $changes = {
417             'daily_close' => {
418             to => '20h55m',
419             rule => $rule,
420             }};
421             }
422              
423             return $changes;
424             }
425 45     45 1 7113  
426             =head2 closes_early_on
427              
428 45 100       895 ->closes_early_on($exchange_object, $date_object);
429              
430 15         270 Returns the closing time as a L<Date::Utility> instance if the exchange closes early on the given date,
431             or C<undef>.
432 15         21  
433 15 100 100     281 =cut
434 4         80  
435             my ($self, $exchange, $when) = @_;
436              
437             return undef unless $self->trades_on($exchange, $when);
438              
439             my $closes_early;
440             my $listed = $self->_get_partial_trading_for($exchange, 'early_closes', $when);
441 15         329 if ($listed) {
442             $closes_early = $when->truncate_to_day->plus_time_interval($listed);
443             } elsif (my $scheduled_changes = $self->regularly_adjusts_trading_hours_on($exchange, $when)) {
444             $closes_early = $when->truncate_to_day->plus_time_interval($scheduled_changes->{daily_close}->{to})
445             if ($scheduled_changes->{daily_close});
446             }
447              
448             return $closes_early;
449             }
450              
451             =head2 opens_late_on
452              
453             ->opens_late_on($exchange_object, $date_object);
454 27     27 1 5191  
455             Returns true if the exchange opens late on the given date.
456 27 100       47  
457             =cut
458 23         32  
459 23         51 my ($self, $exchange, $when) = @_;
460 23 100       103  
    100          
461 3         8 return undef unless $self->trades_on($exchange, $when);
462              
463             my $opens_late;
464 2 50       12 my $listed = $self->_get_partial_trading_for($exchange, 'late_opens', $when);
465             if ($listed) {
466             $opens_late = $when->truncate_to_day->plus_time_interval($listed);
467 23         939 } elsif (my $scheduled_changes = $self->regularly_adjusts_trading_hours_on($exchange, $when)) {
468             $opens_late = $when->truncate_to_day->plus_time_interval($scheduled_changes->{daily_open}->{to})
469             if ($scheduled_changes->{daily_open});
470             }
471              
472             return $opens_late;
473             }
474              
475             =head2 seconds_of_trading_between_epochs
476              
477             ->seconds_of_trading_between_epochs($exchange_object, $epoch1, $epoch2);
478              
479 34     34 1 1010 Get total number of seconds of trading time between two epochs accounting for breaks.
480              
481 34 100       56 =cut
482              
483 23         28 my $full_day = 86400;
484 23         54  
485 23 100       83 my ($self, $exchange, $start, $end) = @_;
    50          
486 2         6  
487             my ($start_epoch, $end_epoch) = ($start->epoch, $end->epoch);
488             my $result = 0;
489 0 0       0  
490             # step 1: calculate non-cached incomplete start-day and end_dates
491             my $day_start = $start_epoch - ($start_epoch % $full_day);
492 23         586 my $day_end = $end_epoch - ($end_epoch % $full_day);
493             if (($day_start != $start_epoch) && ($start_epoch < $end_epoch)) {
494             $result += $self->_computed_trading_seconds($exchange, $start_epoch, min($day_start + 86399, $end_epoch));
495             $start_epoch = $day_start + $full_day;
496             }
497             if (($day_end != $end_epoch) && ($start_epoch < $end_epoch)) {
498             $result += $self->_computed_trading_seconds($exchange, max($start_epoch, $day_end), $end_epoch);
499             $end_epoch = $day_end;
500             }
501              
502             # step 2: calculate intermediated values (which are guaranteed to be day-boundary)
503             # with cache-aware way
504             if ($start_epoch < $end_epoch) {
505             $result += $self->_seconds_of_trading_between_epochs_days_boundary($exchange, $start_epoch, $end_epoch);
506 22     22 1 23113 }
507              
508 22         543 return $result;
509 22         203 }
510              
511             =head2 regular_trading_day_after
512 22         37  
513 22         28 ->regular_trading_day_after($exchange_object, $date_object);
514 22 100 66     79  
515 17         54 Returns a Date::Utility object on a trading day where the exchange does not close early or open late after the given date.
516 17         26  
517             =cut
518 22 100 100     74  
519 10         29 my ($self, $exchange, $when) = @_;
520 10         14  
521             return undef if $self->closing_on($exchange, $when);
522              
523             my $counter = 0;
524             my $regular_trading_day = $self->trade_date_after($exchange, $when);
525 22 100       38 while ($counter <= 10) {
526 7         16 my $possible = $regular_trading_day->plus_time_interval($counter . 'd');
527             if ( not $self->closes_early_on($exchange, $possible)
528             and not $self->opens_late_on($exchange, $possible)
529 22         404 and $self->trades_on($exchange, $possible))
530             {
531             $regular_trading_day = $possible;
532             last;
533             }
534             $counter++;
535             }
536              
537             return $regular_trading_day;
538             }
539              
540             =head2 trading_period
541 2     2 1 5554  
542             ->trading_period('HKSE', Date::Utility->new);
543 2 50       6  
544             Returns an array reference of hash references of open and close time of the given exchange and epoch
545 2         3  
546 2         7 =cut
547 2         8  
548 2         8 my ($self, $exchange, $when) = @_;
549 2 50 33     152  
      33        
550             return [] if not $self->trades_on($exchange, $when);
551             my $open = $self->opening_on($exchange, $when);
552             my $close = $self->closing_on($exchange, $when);
553 2         4 my $breaks = $self->trading_breaks($exchange, $when);
554 2         4  
555             my @times = ($open);
556 0         0 if (defined $breaks) {
557             push @times, @{$_} for @{$breaks};
558             }
559 2         6 push @times, $close;
560              
561             my @periods;
562             for (my $i = 0; $i < $#times; $i += 2) {
563             push @periods,
564             {
565             open => $times[$i]->epoch,
566             close => $times[$i + 1]->epoch
567             };
568             }
569              
570             return \@periods;
571 2     2 1 4577 }
572              
573 2 50       8 =head2 is_holiday_for
574 2         8  
575 2         6 Check if it is a holiday for a specific exchange or a country on a specific day
576 2         12  
577             ->is_holiday_for('ASX', '2013-01-01'); # Australian exchange holiday
578 2         4 ->is_holiday_for('USD', Date::Utility->new); # United States country holiday
579 2 100       6  
580 1         2 Returns the description of the holiday if it is a holiday.
  1         4  
  1         3  
581              
582 2         4 =cut
583              
584 2         3 my ($self, $symbol, $date) = @_;
585 2         7  
586 3         69 return $self->_get_holidays_for($symbol, $date);
587             }
588              
589             =head2 is_in_dst_at
590              
591             ->is_in_dst_at($exchange_object, $date_object);
592              
593 2         25 Is this exchange trading on daylight savings times for the given epoch?
594              
595             =cut
596              
597             my ($self, $exchange, $epoch) = @_;
598              
599             return Date::Utility->new($epoch)->is_dst_in_zone($exchange->trading_timezone);
600             }
601              
602             ### PRIVATE ###
603              
604             my ($self, $symbol, $when) = @_;
605              
606             my $date = $when->truncate_to_day->epoch;
607             my $calendar = $self->calendar->{holidays};
608 74     74 1 8362 my $holiday = $calendar->{$date};
609              
610 74         141 return undef unless $holiday;
611              
612             foreach my $holiday_desc (keys %$holiday) {
613             return $holiday_desc if (first { $symbol eq $_ } @{$holiday->{$holiday_desc}});
614             }
615              
616             return undef;
617             }
618              
619             my ($self, $exchange, $when) = @_;
620              
621             $when = Date::Utility->new($when);
622 134     134 1 17464 my $in_trading_break = 0;
623             if (my $breaks = $self->trading_breaks($exchange, $when)) {
624 134         282 foreach my $break_interval (@{$breaks}) {
625             if ($when->epoch >= $break_interval->[0]->epoch and $when->epoch <= $break_interval->[1]->epoch) {
626             $in_trading_break++;
627             last;
628             }
629             }
630 74     74   105 }
631              
632 74         142 return $in_trading_break;
633 74         3336 }
634 74         118  
635             =head2 get_exchange_open_times
636 74 100       239  
637             Query an exchange for valid opening times. Expects 3 parameters:
638 24         63  
639 24 100   31   93 =over 4
  31         158  
  24         75  
640              
641             =item * C<$exchange> - a L<Finance::Exchange> instance
642 2         8  
643             =item * C<$date> - a L<Date::Utility>
644              
645             =item * C<$which> - which market information to request, see below
646 9     9   21  
647             =back
648 9         21  
649 9         66 The possible values for C<$which> include:
650 9 100       28  
651 1         2 =over 4
  1         2  
652 1 50 33     22  
653 1         47 =item * C<daily_open>
654 1         3  
655             =item * C<daily_close>
656              
657             =item * C<trading_breaks>
658              
659 9         38 =back
660              
661             Returns either C<undef>, a single L<Date::Utility>, or an arrayref of L<Date::Utility> instances.
662              
663             =cut
664              
665             my ($self, $exchange, $date, $which) = @_;
666              
667             my $when = (ref $date) ? $date : Date::Utility->new($date);
668             my $that_midnight = $self->trading_date_for($exchange, $when);
669             my $requested_time;
670             if ($self->trades_on($exchange, $that_midnight)) {
671             my $dst_key = $self->_times_dst_key($exchange, $that_midnight);
672             my $ti = $exchange->market_times->{$dst_key}->{$which};
673             my $extended_lunch_hour;
674             if ($which eq 'trading_breaks') {
675             my $extended_trading_breaks = $exchange->market_times->{$dst_key}->{day_of_week_extended_trading_breaks};
676             $extended_lunch_hour = ($extended_trading_breaks and $when->day_of_week == $extended_trading_breaks) ? 1 : 0;
677             }
678             if ($ti) {
679             if (ref $ti eq 'ARRAY') {
680             my $trading_breaks = $extended_lunch_hour ? @$ti[1] : @$ti[0];
681             my $start_of_break = $that_midnight->plus_time_interval($trading_breaks->[0]);
682             my $end_of_break = $that_midnight->plus_time_interval($trading_breaks->[1]);
683             push @{$requested_time}, [$start_of_break, $end_of_break];
684             } else {
685             $requested_time = $that_midnight->plus_time_interval($ti);
686             }
687             }
688             }
689             return $requested_time; # returns null on no trading days.
690             }
691              
692             my ($self, $exchange, $when) = @_;
693 112     112 1 193  
694             my $epoch = (ref $when) ? $when->epoch : $when;
695 112 50       223  
696 112         189 return 'dst' if $self->is_in_dst_at($exchange, $epoch);
697 112         2124 return 'standard';
698 112 100       195 }
699 97         183  
700 97         7093 # get partial trading data for a given exchange
701 97         701 my ($self, $exchange, $type, $when) = @_;
702 97 100       191  
703 64         1177 my $cached = $self->calendar->{$type};
704 64 50 33     513 my $date = $when->truncate_to_day->epoch;
705             my $partial_defined = $cached->{$date};
706 97 100       162  
707 84 100       162 return undef unless $partial_defined;
708 51 50       102  
709 51         127 foreach my $close_time (keys %{$cached->{$date}}) {
710 51         2314 my $symbols = $cached->{$date}{$close_time};
711 51         1953 return $close_time if (first { $exchange->symbol eq $_ } @$symbols);
  51         138  
712             }
713 33         79  
714             return undef;
715             }
716              
717 112         4381 my ($self, $begin, $end) = @_;
718              
719             my @days_between = ();
720              
721 132     132   442 # Don't include start and end days.
722             my $current = Date::Utility->new($begin)->truncate_to_day->plus_time_interval('1d');
723 132 50       2379 $end = Date::Utility->new($end)->truncate_to_day->minus_time_interval('1d');
724              
725 132 100       723 # Generate all days between.
726 99         11211 while (not $current->is_after($end)) {
727             push @days_between, $current;
728             $current = $current->plus_time_interval('1d'); # Next day, please!
729             }
730              
731 46     46   82 return \@days_between;
732             }
733 46         928  
734 46         103 Memoize::memoize('_days_between', NORMALIZER => '_normalize_on_just_dates');
735 46         1373  
736             ## PRIVATE _market_opens
737 46 100       100 #
738             # PARAMETERS :
739 5         7 # - time : the time as a timestamp
  5         20  
740 5         10 #
741 5 50   5   30 # RETURNS : A reference to a hash with the following keys:
  5         137  
742             # - open : is set to 1 if the market is currently open, 0 if market is closed
743             # but will open, 'undef' if market is closed and will not open again
744 0         0 # today.
745             # - closed : undefined if market has not been open yet, otherwise contains the
746             # seconds for how long the market was closed.
747             # - opens : undefined if market is currently open and does not open anymore today,
748             # otherwise the market will open in 'opens' seconds.
749             # - closes : undefined if open is undef, otherwise market will close in 'closes' seconds.
750             # - opened : undefined if market is closed, contains the seconds the market has
751             # been open.
752             #
753             #
754             ########
755             my ($self, $exchange, $when) = @_;
756              
757             my $date = $when;
758             # Figure out which "trading day" we are on
759             # even if it differs from the GMT calendar day.
760             my $next_day = $date->plus_time_interval('1d')->truncate_to_day;
761             my $next_open = $self->opening_on($exchange, $next_day);
762             $date = $next_day if ($next_open and not $date->is_before($next_open));
763              
764             my $open = $self->opening_on($exchange, $date);
765             my $close = $self->closing_on($exchange, $date);
766              
767             if (not $open) {
768              
769             # date is not a trading day: will not and has not been open today
770             my $next_open = $self->opening_on($exchange, $self->trade_date_after($exchange, $when));
771             return {
772             open => undef,
773             opens => $next_open->epoch - $when->epoch,
774             opened => undef,
775             closes => undef,
776             closed => undef,
777             };
778             }
779              
780             my $breaks = $self->trading_breaks($exchange, $when);
781             # not trading breaks
782             if (not $breaks) {
783             # Past closing time: opens next trading day, and has been open today
784             if ($close and not $when->is_before($close)) {
785             return {
786             open => undef,
787 10     10   14 opens => undef,
788             opened => $when->epoch - $open->epoch,
789 10         18 closes => undef,
790             closed => $when->epoch - $close->epoch,
791             };
792 10         25 } elsif ($when->is_before($open)) {
793 10         1771 return {
794 10 50 66     32 open => 0,
795             opens => $open->epoch - $when->epoch,
796 10         38 opened => undef,
797 10         22 closes => $close->epoch - $when->epoch,
798             closed => undef,
799 10 100       23 };
800             } elsif ($when->is_same_as($open) or ($when->is_after($open) and $when->is_before($close)) or $when->is_same_same($close)) {
801             return {
802 2         7 open => 1,
803             opens => undef,
804 2         36 opened => $when->epoch - $open->epoch,
805             closes => $close->epoch - $when->epoch,
806             closed => undef,
807             };
808             }
809             } else {
810             my @breaks = @$breaks;
811             # Past closing time: opens next trading day, and has been open today
812 8         21 if ($close and not $when->is_before($close)) {
813             return {
814 8 100       19 open => undef,
815             opens => undef,
816 4 100 66     15 opened => $when->epoch - $breaks[-1][1]->epoch,
    50 0        
    0 0        
      0        
817             closes => undef,
818 2         49 closed => $when->epoch - $close->epoch,
819             };
820             } elsif ($when->is_before($open)) {
821             return {
822             open => 0,
823             opens => $open->epoch - $when->epoch,
824             opened => undef,
825             closes => $breaks[0][0]->epoch - $when->epoch,
826 2         61 closed => undef,
827             };
828             } else {
829             my $current_open = $open;
830             for (my $i = 0; $i <= $#breaks; $i++) {
831             my $int_open = $breaks[$i][0];
832             my $int_close = $breaks[$i][1];
833             my $next_open = exists $breaks[$i + 1] ? $breaks[$i + 1][0] : $close;
834 0         0  
835             if ($when->is_before($int_open)
836             and ($when->is_same_as($current_open) or $when->is_after($current_open)))
837             {
838             return {
839             open => 1,
840             opens => undef,
841             opened => $when->epoch - $current_open->epoch,
842 4         9 closes => $int_open->epoch - $when->epoch,
843             closed => undef,
844 4 100 66     16 };
    50          
845             } elsif ($when->is_same_as($int_open)
846 1         28 or ($when->is_after($int_open) and $when->is_before($int_close))
847             or $when->is_same_as($int_close))
848             {
849             return {
850             open => 0,
851             opens => $int_close->epoch - $when->epoch,
852             opened => undef,
853             closes => $close->epoch - $when->epoch, # we want to know seconds to official close
854 0         0 closed => $when->epoch - $int_open->epoch,
855             };
856             } elsif ($when->is_after($int_close) and $when->is_before($next_open)) {
857             return {
858             open => 1,
859             opens => undef,
860             opened => $when->epoch - $int_close->epoch,
861 3         41 closes => $next_open->epoch - $when->epoch,
862 3         11 closed => undef,
863 3         7 };
864 3         4 }
865 3 50       7 }
866              
867 3 100 33     8 }
    50 66        
    0 33        
      33        
      33        
      0        
868             }
869              
870             return undef;
871 1         41 }
872              
873             ## PRIVATE method _seconds_of_trading_between_epochs_days_boundary
874             #
875             # there is a strict assumption, that start and end epoch are day boundaries
876             #
877             my %cached_seconds_for_interval; # key ${epoch1}-${epoch2}, value: seconds
878              
879             my ($self, $exchange, $start_epoch, $end_epoch) = @_;
880              
881             my $cache_key = join('-', $exchange->symbol, $start_epoch, $end_epoch);
882 2         104 my $result = $cached_seconds_for_interval{$cache_key} //= do {
883             my $head = $self->_computed_trading_seconds($exchange, $start_epoch, $start_epoch + 86399);
884             if ($end_epoch - $start_epoch > $full_day - 1) {
885             my $tail = $self->_seconds_of_trading_between_epochs_days_boundary($exchange, $start_epoch + $full_day, $end_epoch);
886             $head + $tail;
887             }
888             };
889              
890 0         0 return $result;
891             }
892              
893             ## PRIVATE method _computed_trading_seconds
894             #
895             # This one ACTUALLY does the heavy lifting of determining the number of trading seconds in an intraday period.
896             #
897             my ($self, $exchange, $start, $end) = @_;
898              
899             my $total_trading_time = 0;
900             my $when = Date::Utility->new($start);
901              
902 0         0 if ($self->trades_on($exchange, $when)) {
903              
904             # Do the full computation.
905             my $opening_epoch = $self->opening_on($exchange, $when)->epoch;
906             my $closing_epoch = $self->closing_on($exchange, $when)->epoch;
907              
908             # Total trading time left in interval. This is always between 0 to $period_secs_basis.
909             # This will automatically take care of early close because market close will just be the early close time.
910             my $total_trading_time_including_lunchbreaks =
911             max(min($closing_epoch, $end), $opening_epoch) - min(max($opening_epoch, $start), $closing_epoch);
912 23     23   41  
913             my $total_lunch_break_time = 0;
914 23         439  
915 23   66     185 # Now take care of lunch breaks. But handle early close properly. It could be that
916 22         47 # the early close already wipes out the need to handle lunch breaks.
917 22 100       68 # Handle early close. For example on 24 Dec 2009, HKSE opens at 2:00, and stops
918 16         53 # for lunch at 4:30 and never reopens. In that case the value of $self->closing_on($thisday)
919 16         23 # is 4:30, and lunch time between 4:30 to 6:00 is no longer relevant.
920             if (my $breaks = $self->trading_breaks($exchange, $when)) {
921             for my $break_interval (@{$breaks}) {
922             my $interval_open = $break_interval->[0];
923 23         40 my $interval_close = $break_interval->[1];
924             my $close_am = min($interval_open->epoch, $closing_epoch);
925             my $open_pm = min($interval_close->epoch, $closing_epoch);
926              
927             $total_lunch_break_time = max(min($open_pm, $end), $close_am) - min(max($close_am, $start), $open_pm);
928              
929             if ($total_lunch_break_time < 0) {
930             die 'Total lunch break time between ' . $start . '] and [' . $end . '] for exchange[' . $self->exchange->symbol . '] is negative';
931 49     49   73 }
932             }
933 49         55 }
934 49         107  
935             $total_trading_time = $total_trading_time_including_lunchbreaks - $total_lunch_break_time;
936 49 100       1358 if ($total_trading_time < 0) {
937             croak 'Total trading time (minus lunch) between '
938             . $start
939 42         94 . '] and ['
940 42         238 . $end
941             . '] for exchange['
942             . $self->exchange->symbol
943             . '] is negative.';
944 42         297 }
945             }
946              
947 42         51 return $total_trading_time;
948             }
949              
950             ## PRIVATE static methods
951             #
952             # Many of these functions don't change their results if asked for the
953             # same dates many times. Let's exploit that for time over space
954 42 50       90 #
955 42         51 # This actually comes up in our pricing where we have to do many interpolations
  42         68  
956 42         56 # over the same ranges on different values.
957 42         50 #
958 42         816 # This attaches to the static method on the class for the lifetime of this instance.
959 42         901 # Since we only want the cache for our specific symbol, we need to include an identifier.
960              
961 42         251 my ($self, @dates) = @_;
962              
963 42 50       97 return join '|', (map { Date::Utility->new($_)->days_since_epoch } @dates);
964 0         0 }
965              
966             no Moose;
967             __PACKAGE__->meta->make_immutable;
968              
969 42         54 1;