File Coverage

blib/lib/Finance/FITF.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::FITF;
2              
3 3     3   1456 use strict;
  3         5  
  3         116  
4 3     3   85 use 5.008_001;
  3         11  
  3         367  
5             our $VERSION = '0.31';
6 3     3   1962 use Finance::FITF::Writer;
  0            
  0            
7             use POSIX qw(ceil);
8             BEGIN {
9             eval "use Class::XSAccessor::Compat 'antlers'; 1" or
10             eval "use Class::Accessor::Fast 'antlers'; 1" or die $@;
11             }
12              
13             use Sub::Exporter -setup => {
14             groups => {
15             default => [ qw(FITF_TICK_FMT FITF_TICK_NONE FITF_TICK_USHORT FITF_TICK_ULONG
16             FITF_BAR_FMT FITF_BAR_USHORT FITF_BAR_ULONG)],
17             },
18             exports => [qw(FITF_TICK_FMT FITF_TICK_NONE FITF_TICK_USHORT FITF_TICK_ULONG
19             FITF_BAR_FMT FITF_BAR_USHORT FITF_BAR_ULONG)],
20             };
21              
22             use constant FITF_TICK_FMT => 0x000f;
23             use constant FITF_TICK_NONE => 0x0000;
24             use constant FITF_TICK_USHORT => 0x0001;
25             use constant FITF_TICK_ULONG => 0x0002;
26              
27             use constant FITF_BAR_FMT => 0x00f0;
28             use constant FITF_BAR_USHORT => 0x0010;
29             use constant FITF_BAR_ULONG => 0x0020;
30              
31             use constant FITF_VERSION => 0x02;
32             use constant FITF_MAGIC => "\x1f\xf1";
33             use Parse::Binary::FixedFormat;
34              
35             my $header_fmt = Parse::Binary::FixedFormat->new(
36             [qw(magic:a2 version:n
37             date:a8
38             time_zone:Z31
39             start:N:3
40             end:N:3
41             records:N
42             bar_seconds:n
43             format:N
44             divisor:N
45             name:Z47
46             )]);
47              
48             my $bar_s =
49             Parse::Binary::FixedFormat->new([qw(
50             open:n
51             high:n
52             low:n
53             close:n
54             volume:n
55             ticks:n
56             index:N
57             )]);
58              
59             my $bar_l =
60             Parse::Binary::FixedFormat->new([qw(
61             open:N
62             high:N
63             low:N
64             close:N
65             volume:N
66             ticks:N
67             index:N
68             )]);
69              
70              
71             my $tick_s =
72             Parse::Binary::FixedFormat->new([qw(
73             offset_min:s
74             offset_msec:n
75             price:n
76             volume:n
77             )]);
78              
79             my $tick_l =
80             Parse::Binary::FixedFormat->new([qw(
81             offset_min:s
82             offset_msec:n
83             price:N
84             volume:N
85             )]);
86              
87             has fh => ( is => 'ro' );
88              
89             has header => ( is => "ro", isa => "HashRef" );
90              
91             has header_fmt => ( is => "ro", isa => "Parse::Binary::FixedFormat" );
92             has header_sz => ( is => "rw", isa => "Int");
93             has bar_fmt => ( is => "ro", isa => "Parse::Binary::FixedFormat" );
94             has bar_sz => ( is => "rw", isa => "Int");
95             has tick_fmt => ( is => "ro", isa => "Parse::Binary::FixedFormat" );
96             has tick_sz => ( is => "rw", isa => "Int");
97              
98             has day => (is => "rw", isa => "DateTime");
99             has date_start => (is => "rw", isa => "Int");
100              
101             has nbars => (is => "rw", isa => "Int");
102              
103             sub new {
104             my $class = shift;
105             my $self = $class->SUPER::new(@_);
106              
107             $self->header_sz( length( $self->header_fmt->format({}) ) );
108             $self->bar_sz( length( $self->bar_fmt->format({}) ) );
109             $self->tick_sz( length( $self->tick_fmt->format({}) ) );
110             my ($y, $m, $d) = $self->header->{date} =~ m/(\d\d\d\d)(\d\d)(\d\d)/;
111             $self->day(DateTime->new(time_zone => $self->header->{time_zone},
112             year => $y, month => $m, day => $d));
113             $self->date_start( $self->day->epoch );
114              
115             $self->{bar_ts} ||= [];
116              
117             for (0..2) {
118             my ($start, $end) = ($self->header->{start}[$_], $self->header->{end}[$_]);
119             last unless $start && $end;
120              
121             push @{$self->{bar_ts}},
122             map { $start + $_ * $self->{header}{bar_seconds} }
123             (1..($end - $start) / $self->{header}{bar_seconds});
124             }
125             $self->nbars( scalar @{$self->{bar_ts}} );
126              
127             return $self;
128             }
129              
130              
131             sub new_from_file {
132             my $class = shift;
133             my $file = shift;
134             open my $fh, '<:raw', $file or die "$file: $!";
135              
136             sysread $fh, my $buf, length( $header_fmt->format({}) );
137              
138             my $header = $header_fmt->unformat($buf);
139              
140             # check magic
141             die "file not recognized" unless $header->{magic} eq FITF_MAGIC;
142             # XXX: sanity check for format
143              
144             my $self = $class->new({
145             header_fmt => $header_fmt,
146             bar_fmt => ($header->{format} & FITF_BAR_FMT) == FITF_BAR_USHORT ? $bar_s : $bar_l,
147             tick_fmt => ($header->{format} & FITF_TICK_FMT) == FITF_TICK_USHORT ? $tick_s : $tick_l,
148             fh => $fh,
149             header => $header });
150              
151             return $self;
152             }
153              
154             sub bar_idx {
155             my ($self, $timestamp) = @_;
156             my $session_idx = 0;
157             my $h = $self->header;
158             my $offset = 0;
159             while ($session_idx < 3 && $timestamp > $h->{end}[$session_idx]) {
160             $offset += ($h->{end}[$session_idx] - $h->{start}[$session_idx]) / $h->{bar_seconds};
161             ++$session_idx;
162             }
163             return if $session_idx == 3;
164             my $nth = ($timestamp - $h->{start}[$session_idx]) / $h->{bar_seconds} + $offset - 1;
165             }
166              
167             sub bar_at {
168             my ($self, $timestamp) = @_;
169             my $nth = $self->bar_idx($timestamp);
170             return unless defined $nth;
171              
172             seek $self->{fh}, $nth * $self->bar_sz + $self->header_sz, 0;
173              
174             my $buf;
175             sysread $self->{fh}, $buf, $self->bar_sz;
176             my $bar = $self->bar_fmt->unformat($buf);
177             $bar->{$_} /= $self->{header}{divisor} for qw(open high low close);
178             return $bar;
179             }
180              
181             sub run_ticks {
182             my ($self, $start, $end, $cb) = @_;
183             my $cnt = $end - $start + 1;
184             seek $self->{fh}, $start * $self->tick_sz + $self->nbars * $self->bar_sz + $self->header_sz, 0;
185              
186             $self->_fast_unformat($self->tick_fmt, $self->tick_sz, $cnt,
187             sub {
188             my $tick = shift;
189             my $time = $self->{date_start} + $tick->{offset_min}*60 + $tick->{offset_msec}/1000;
190             $cb->($time, $tick->{price} / $self->{header}{divisor}, $tick->{volume});
191             });
192             }
193              
194             sub run_bars {
195             my ($self, $start, $end, $cb) = @_;
196             my $cnt = $end - $start + 1;
197             seek $self->{fh}, $start * $self->bar_sz + $self->header_sz, 0;
198              
199             $self->_fast_unformat($self->bar_fmt, $self->bar_sz, $cnt, $cb);
200             }
201              
202             sub _fast_unformat {
203             my ($self, $fmt, $sz, $n, $cb) = @_;
204              
205             my $buf;
206             read $self->{fh}, $buf, $sz * $n;
207              
208             my @records = unpack('('.$fmt->_format.')*', $buf);
209             while (my @r = splice(@records, 0, scalar @{$fmt->{Names}})) {
210             my $record = {};
211             @{$record}{@{$fmt->{Names}}} = @r;
212             $cb->($record);
213             }
214             }
215              
216             sub run_bars_as {
217             my ($self, $bar_seconds, $offset, $cb) = @_;
218             Carp::confess unless $bar_seconds;
219             my @ts;
220             my $h = $self->header;
221             for (0..2) {
222             my ($start, $end) = ($self->header->{start}[$_], $self->header->{end}[$_]);
223             last unless $start && $end;
224              
225             $start -= $offset;
226             push @ts,
227             map { my $t = $start + $_ * $bar_seconds;
228             $t < $end ? $t : $end;
229             } (1..ceil(($end - $start) / $bar_seconds));
230             }
231              
232             my $i = 0;
233             my $current_bar;
234             my $last_price;
235             $self->run_bars(0, $self->nbars-1,
236             sub {
237             my $bar = shift;
238             my $ts = $self->{bar_ts}[$i++];
239             if ($bar->{volume}) {
240             if ($current_bar) {
241             $current_bar->{high} = $bar->{high}
242             if $bar->{high} > $current_bar->{high};
243             $current_bar->{low} = $bar->{low}
244             if $bar->{low} < $current_bar->{low};
245              
246             $current_bar->{close} = $bar->{close};
247             $current_bar->{volume} += $bar->{volume};
248             $current_bar->{ticks} += $bar->{ticks};
249             }
250             else {
251             $current_bar = $bar;
252             }
253             }
254             if ($ts == $ts[0]) {
255             $current_bar ||= { open => $last_price,
256             high => $last_price,
257             low => $last_price,
258             close => $last_price,
259             volume => 0,
260             ticks => 0,
261             };
262             $cb->(shift @ts, $current_bar);
263             $last_price = $current_bar->{close};
264             undef $current_bar;
265             }
266             });
267             if (@ts) {
268             $cb->(shift @ts, $current_bar);
269             }
270             }
271              
272             sub format_timestamp {
273             my ($self, $ts) = @_;
274             my $hms = $ts - $self->{date_start};
275             my $d = $self->day;
276              
277             if ($hms < 0) {
278             $d = $d->clone->subtract(days => 1);
279             $hms += 86400;
280             }
281             elsif ($hms >= 86400) {
282             $d = $d->clone->add(days => 1);
283             $hms -= 86400;
284             }
285             $hms = sprintf('%02d:%02d:%02d',
286             int($hms / 60 / 60),
287             int(($hms % 3600)/60),
288             ($hms % 60));
289             return $d->ymd. ' '.$hms;
290             }
291              
292             sub new_writer {
293             my ($class, %args) = @_;
294             my $hdr = delete $args{header};
295             my $header = {
296             magic => FITF_MAGIC,
297             version => FITF_VERSION,
298             start => [],
299             end => [],
300             records => 0,
301             bar_seconds => 10,
302             divisor => 1,
303             format => FITF_TICK_ULONG | FITF_BAR_ULONG,
304             %$hdr,
305             };
306              
307             Finance::FITF::Writer->new({
308             header_fmt => $header_fmt,
309             bar_fmt => ($header->{format} & FITF_BAR_FMT) == FITF_BAR_USHORT ? $bar_s : $bar_l,
310             tick_fmt => ($header->{format} & FITF_TICK_FMT) == FITF_TICK_USHORT ? $tick_s : $tick_l,
311             %args,
312             header => $header});
313             }
314              
315             1;
316             __END__