File Coverage

blib/lib/Finance/TW/TAIFEX.pm
Criterion Covered Total %
statement 95 131 72.5
branch 18 38 47.3
condition 6 26 23.0
subroutine 27 32 84.3
pod 11 11 100.0
total 157 238 65.9


line stmt bran cond sub pod time code
1             package Finance::TW::TAIFEX;
2 4     4   131362 use strict;
  4         9  
  4         124  
3 4     4   3462 use Any::Moose;
  4         176518  
  4         29  
4 4     4   11530 use DateTime;
  4         883683  
  4         292  
5 4     4   5952 use DateTime::Format::Strptime;
  4         41418  
  4         539  
6 4     4   100 use Try::Tiny;
  4         9  
  4         266  
7 4     4   5150 use File::ShareDir qw(dist_dir);
  4         29881  
  4         361  
8 4     4   41 use List::MoreUtils qw(firstidx);
  4         11  
  4         462  
9 4     4   24 use Any::Moose 'X::Types::DateTime';
  4         9  
  4         41  
10             require MouseX::NativeTraits if Any::Moose->mouse_is_preferred;
11 4     4   1272688 use HTTP::Request::Common qw(POST);
  4         114024  
  4         430  
12 4     4   6287 use LWP::Simple 'getstore';
  4         243287  
  4         58  
13 4     4   6451 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  4         381824  
  4         800  
14              
15 4     4   2507 use Finance::TW::TAIFEX::Product;
  4         15  
  4         140  
16 4     4   2641 use Finance::TW::TAIFEX::Contract;
  4         13  
  4         126  
17              
18 4     4   121 use 5.008_001;
  4         15  
  4         13584  
19             our $VERSION = '0.39';
20              
21             has context_date => ( is => "rw", isa => "DateTime",
22             default => sub { DateTime->now(time_zone => 'Asia/Taipei') },
23             coerce => 1);
24              
25             has calendar => (is => "ro", isa => "HashRef", default => sub { {} });
26              
27             has products => (
28             traits => ['Hash'],
29             is => "ro",
30             isa => "HashRef[Finance::TW::TAIFEX::Product]",
31             handles => {
32             has_product => 'exists',
33             product => 'get',
34             },
35             lazy_build => 1,
36             );
37              
38              
39             sub _build_products {
40 1     1   35 my $self = shift;
41             return {
42 1         3 (map { $_ => Finance::TW::TAIFEX::Product->new_with_traits(
  15         14433  
43             traits => ['Settlement::ThirdWednesday'],
44             exchange => $self,
45             name => $_,
46             ) } qw(TX MTX TE TF T5F MSF CPF XIF GTF
47             TXO TEO TFO MSO XIO GTO)),
48             };
49              
50             # (map { $_ => Finance::TW::TAIFEX::Product->new_with_traits(
51             # traits => ['Settlement::ThirdToLastDayOfMonth'],
52             # exchange => $self,
53             # name => $_,
54             # ) } qw(TGF TGO)),
55             #
56             # (GBF => Finance::TW::TAIFEX::Product->new_with_traits(
57             # traits => ['Settlement::SecondWednesday'],
58             # exchange => $self,
59             # name => 'GBF',
60             # )),
61              
62             }
63              
64             =head1 NAME
65              
66             Finance::TW::TAIFEX - Helper functions for Taiwan Futures Exchange
67              
68             =head1 SYNOPSIS
69              
70             use Finance::TW::TAIFEX;
71              
72             my $taifex = Finance::TW::TAIFEX->new();
73              
74             $taifex->is_trading_day(); # is today a trading day?
75              
76             my $date = DateTime->now;
77             $taifex->daily_futures_uri($date);
78             $taifex->daily_options_uri($date);
79              
80             $taifex->contract('TX', '201001')->settlement_date;
81             $taifex->product('TX')->near_term;
82             $taifex->product('TX')->next_term;
83              
84             =head1 DESCRIPTION
85              
86             Finance::TW::TAIFEX provides useful helper functions for the Taiwan
87             Future Exchanges.
88              
89             =head1 METHODS
90              
91             =head2 product NAME
92              
93             Returns the L<Finance::TW::TAIFEX::Product> object represented by NAME.
94              
95             Currently supported product names:
96              
97             =over
98              
99             =item Futures
100              
101             TX MTX TE TF T5F MSF CPF XIF GTF
102              
103             =item Options
104              
105             TXO TEO TFO MSO XIO GTO
106              
107             =back
108              
109             =head2 has_product NAME
110              
111             Checks if the given product exists.
112              
113             =cut
114              
115             my $Strp = DateTime::Format::Strptime->new( pattern => '%F', time_zone => 'Asia/Taipei');
116              
117             sub BUILDARGS {
118 4     4 1 532 my $class = shift;
119 4 50       18 return { @_ } unless $#_ == 0;
120 4 50       22 return { } unless $_[0];
121 4 50       54 return { context_date => ref $_[0] ? $_[0] : $Strp->parse_datetime($_[0]) }
122             }
123              
124             =head2 contract NAME YEAR MONTH
125              
126             Returns the L<Finance::TW::TAIFEX::Contract> of the given product expires on YEAR/MONTH.
127              
128             =cut
129              
130             sub contract {
131 3     3 1 671 my ($self, $name, $year, $month) = @_;
132 3 100       12 unless ($month) {
133 1         4 my $spec = $year;
134 1 50       15 ($year, $month) = $spec =~ m/^(\d{4})(\d{2})$/
135             or die "$spec doesn't look like a contract month";
136             }
137              
138 2 100       15 die "unknown product $name"
139             unless $self->has_product($name);
140              
141 1         17 return Finance::TW::TAIFEX::Contract->new( product => $self->product($name),
142             year => $year,
143             month => $month );
144             }
145              
146             sub _read_cal {
147 7     7   14 my ($self, $file) = @_;
148 7 50       519 open my $fh, '<', $file or die "$file: $!" ;
149 7         778 return [map { chomp; $_ } <$fh>];
  1747         1600  
  1747         4713  
150             }
151              
152             =head2 calendar_for YEAR
153              
154             Returns the trading calendar for YEAR.
155              
156             =cut
157              
158             sub calendar_for {
159 34     34 1 179 my ($self, $year) = @_;
160 34   33     88 $year ||= $self->context_date->year;
161              
162 34 100       240 return $self->calendar->{$year}
163             if $self->calendar->{$year};
164              
165 7         636 my $dist_dir = File::Spec->rel2abs("../../../share", File::Basename::dirname($INC{"Finance/TW/TAIFEX.pm"}));
166 7 50   7   313 $dist_dir = try { dist_dir('Finance-TW-TAIFEX') || 'share' } unless -e $dist_dir;
  7 50       279  
167 7         900 $self->calendar->{$year} = $self->_read_cal("$dist_dir/calendar/$year.txt");
168             }
169              
170             =head2 is_trading_day [DATE]
171              
172             Checks if the given DATE is a known trading day. Default DATE is the date in the current context.
173              
174             =cut
175              
176             sub is_trading_day {
177 28     28 1 14955 my ($self, $date) = @_;
178 28   66     108 $date ||= $self->context_date;
179              
180 28         1173 $self->_nth_trading_day($self->calendar_for($date->year), $date->ymd) != -1;
181             }
182              
183             =head2 next_trading_day [DATE]
184              
185             Returns the next known trading day in string after the given DATE.
186              
187             =cut
188              
189             sub next_trading_day {
190 1     1 1 58 my ($self, $date) = @_;
191 1   33     12 $date ||= $self->context_date;
192              
193 1         40 my $cal = $self->calendar_for($date->year);
194 1         40 my $d = $date->ymd;
195 1     247   14 my $nth = firstidx { $_ gt $d } @{$cal};
  247         188  
  1         5  
196              
197 1 50       6 if ($nth < 0) {
198 1         4 return $self->calendar_for($date->year + 1)->[0];
199             }
200              
201 0         0 return $cal->[$nth];
202             }
203              
204             =head2 previous_trading_day [DATE]
205              
206             Returns the previous known trading day in string after the given DATE.
207              
208             =cut
209              
210             sub previous_trading_day {
211 3     3 1 93 my ($self, $date, $offset) = @_;
212 3   33     141 $date ||= $self->context_date;
213 3   50     92 $offset ||= -1;
214              
215 3         12 my $cal = $self->calendar_for($date->year);
216 3         26 my $nth = $self->_nth_trading_day($cal, $date->ymd);
217 3 100       22 die "$date not a known trading day"
218             if $nth < 0;
219              
220 2 100       9 if ($nth + $offset < 0) {
221 1         4 return $self->calendar_for($date->year - 1)->[-1];
222             }
223              
224 1         6 return $cal->[$nth + $offset];
225             }
226              
227             sub _nth_trading_day {
228 31     31   361 my ($self, $cal, $date) = @_;
229 31     6617   122 firstidx { $_ eq $date } @{$cal}
  6617         5919  
  31         190  
230             }
231              
232             =head2 daily_futures_uri DATE
233              
234             Returns the URI of the official TAIFEX futures trading records for DATE.
235              
236             =cut
237              
238             sub daily_futures_uri {
239 0     0 1   my ($self, $date) = @_;
240 0   0       $date ||= $self->context_date;
241 0           return "http://www.taifex.com.tw/DailyDownload/Daily_@{[ $date->ymd('_') ]}.zip";
  0            
242             }
243              
244             =head2 interday_futures_request($product, [$DATE])
245              
246             Returns a HTTP::Request object that fetches futures monthly interday
247             csv file for $product of $DATE.
248              
249             =cut
250              
251             sub interday_futures_request {
252 0     0 1   my ($self, $product, $date) = @_;
253 0   0       $date ||= $self->context_date;
254 0           my $from = $date->clone->truncate( to => 'month' );
255 0           my $to = $from->clone->add( months => 1 )->subtract( days => 1 );
256 0           return POST 'http://www.taifex.com.tw/chinese/3/3_1_2dl.asp',
257             [ goday => '',
258             DATA_DATE => $from->ymd('/'),
259             DATA_DATE1 => $to->ymd('/'),
260             DATA_DATE_Y => $from->year,
261             DATA_DATE_M => $from->month,
262             DATA_DATE_D => $from->day,
263             DATA_DATE_Y1 => $to->year,
264             DATA_DATE_M1 => $to->month,
265             DATA_DATE_D1 => $to->day,
266             commodity_id2t => '',
267             COMMODITY_ID => $product ];
268             }
269              
270             =head2 interday_options_request($product, [$DATE])
271              
272             Returns a HTTP::Request object that fetches options monthly interday
273             csv file for $product of $DATE.
274              
275             =cut
276              
277             sub interday_options_request {
278 0     0 1   my ($self, $product, $date) = @_;
279 0   0       $date ||= $self->context_date;
280 0           my $from = $date->clone->truncate( to => 'month' );
281 0           my $to = $from->clone->add( months => 1 )->subtract( days => 1 );
282 0           return POST 'http://www.taifex.com.tw/chinese/3/3_2_3_b.asp',
283             [ goday => '',
284             DATA_DATE => $from->ymd('/'),
285             DATA_DATE1 => $to->ymd('/'),
286             DATA_DATE_Y => $from->year,
287             DATA_DATE_M => $from->month,
288             DATA_DATE_D => $from->day,
289             DATA_DATE_Y1 => $to->year,
290             DATA_DATE_M1 => $to->month,
291             DATA_DATE_D1 => $to->day,
292             COMMODITY_ID => $product.'%' ];
293             }
294              
295              
296             =head2 daily_options_uri DATE
297              
298             Returns the URI of the official TAIFEX options trading records for DATE.
299              
300             =cut
301              
302             sub daily_options_uri {
303 0     0 1   my ($self, $date) = @_;
304 0   0       $date ||= $self->context_date;
305 0           return "http://www.taifex.com.tw/OptionsDailyDownload/OptionsDaily_@{[ $date->ymd('_') ]}.zip";
  0            
306             }
307              
308             =head2 ensure_rpt DIR, TYPE, PREFIX
309              
310             =cut
311              
312             sub ensure_rpt {
313 0     0 1   my ($self, $rpt_dir, $type, $prefix) = @_;
314 0           my $date = $self->context_date;
315 0           my $rpt_f = "$rpt_dir/".$date->ymd('-').".rpt";
316              
317 0 0         unless (-s $rpt_f) {
318 0           my $rpt = $prefix.$date->ymd('_' ).".rpt";
319 0 0         my $f = $self->can('daily_'.$type.'_uri') or die "unknown type: $type";
320 0           my $url = $self->$f();
321 0           my $tmp = "/tmp/taifex-$type-".$date->ymd('-').".zip";
322 0 0         unless (-s $tmp) {
323 0           my $rc = getstore($url => $tmp);
324              
325 0 0         die("failed to fetch $url: $rc")
326             if HTTP::Status::is_error($rc);
327             }
328 0           my $zip = Archive::Zip->new();
329 0 0         unless ( $zip->read( $tmp ) == AZ_OK ) {
330 0           unlink( $tmp );
331 0           die("Unable to read zip file $tmp");
332             }
333 0 0         unless ( $zip->extractMember($rpt, $rpt_f) == AZ_OK ) {
334 0           die("Unable to extract $rpt from $tmp");
335             }
336             }
337             }
338              
339             =head1 CAVEATS
340              
341             The URI returned by C<daily_futures_uri> and C<daily_options_uri> are only valid for the last 30 trading days per the policy of TAIFEX.
342              
343             =head1 AUTHOR
344              
345             Chia-liang Kao E<lt>clkao@clkao.orgE<gt>
346              
347             =head1 LICENSE
348              
349             This library is free software; you can redistribute it and/or modify
350             it under the same terms as Perl itself.
351              
352             =head1 SEE ALSO
353              
354             L<http://www.taifex.com.tw/>
355              
356             =cut
357              
358             __PACKAGE__->meta->make_immutable;
359 4     4   178 no Any::Moose;
  4         10  
  4         35  
360             1;
361