File Coverage

blib/lib/Finance/Contract.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Finance::Contract;
2              
3 1     1   49643 use strict;
  1         3  
  1         22  
4 1     1   4 use warnings;
  1         2  
  1         36  
5              
6             our $VERSION = '0.010';
7              
8             =head1 NAME
9              
10             Finance::Contract - represents a contract object for a single bet
11              
12             =head1 VERSION
13              
14             version 0.001
15              
16             =head1 SYNOPSIS
17              
18             use feature qw(say);
19             use Finance::Contract;
20             # Create a simple contract
21             my $contract = Finance::Contract->new(
22             contract_type => 'CALLE',
23             duration => '5t',
24             );
25              
26             =head1 DESCRIPTION
27              
28             This is a generic abstraction for financial stock market contracts.
29              
30             =head2 Construction
31              
32             Finance::Contract->new({
33             underlying => 'frxUSDJPY',
34             contract_type => 'CALL',
35             date_start => $now,
36             duration => '5t',
37             currency => 'USD',
38             payout => 100,
39             barrier => 100,
40             });
41              
42             =head2 Dates
43              
44             All date-related parameters:
45              
46             =over 4
47              
48             =item * L</date_pricing>
49              
50             =item * L</date_expiry>
51              
52             =item * L</date_start>
53              
54             =back
55              
56             are L<Date::Utility> instances. You can provide them as epoch values
57             or L<Date::Utility> objects.
58              
59             =cut
60              
61 1     1   430 use Moose;
  1         430628  
  1         6  
62 1     1   8284 use MooseX::Types::Moose qw(Int Num Str);
  0            
  0            
63             use MooseX::Types -declare => ['contract_category'];
64             use Moose::Util::TypeConstraints;
65              
66             use Time::HiRes qw(time);
67             use List::Util qw(min max first);
68             use Scalar::Util qw(looks_like_number);
69             use Math::Util::CalculatedValue::Validatable;
70             use Date::Utility;
71             use Format::Util::Numbers qw(roundcommon);
72             use POSIX qw( floor );
73             use Time::Duration::Concise;
74              
75             # Types used for date+time-related handling
76              
77             use Finance::Contract::Category;
78              
79             use constant _FOREX_BARRIER_MULTIPLIER => 1e6;
80              
81             unless (find_type_constraint('time_interval')) {
82             subtype 'time_interval', as 'Time::Duration::Concise';
83             coerce 'time_interval', from 'Str', via { Time::Duration::Concise->new(interval => $_) };
84             }
85              
86             unless (find_type_constraint('date_object')) {
87             subtype 'date_object', as 'Date::Utility';
88             coerce 'date_object', from 'Str', via { Date::Utility->new($_) };
89             }
90              
91             my @date_attribute = (
92             isa => 'date_object',
93             lazy_build => 1,
94             coerce => 1,
95             );
96              
97             =head1 ATTRIBUTES
98              
99             These are the parameters we expect to be passed when constructing a new contract.
100              
101             =cut
102              
103             =head2 bet_type
104              
105             (required) The type of this contract as an upper-case string.
106              
107             Current types include:
108              
109             =over 4
110              
111             =item * ASIAND
112              
113             =item * ASIANU
114              
115             =item * CALL
116              
117             =item * CALLE
118              
119             =item * DIGITDIFF
120              
121             =item * DIGITEVEN
122              
123             =item * DIGITMATCH
124              
125             =item * DIGITODD
126              
127             =item * DIGITOVER
128              
129             =item * DIGITUNDER
130              
131             =item * EXPIRYMISS
132              
133             =item * EXPIRYMISSE
134              
135             =item * EXPIRYRANGE
136              
137             =item * EXPIRYRANGEE
138              
139             =item * NOTOUCH
140              
141             =item * ONETOUCH
142              
143             =item * PUT
144              
145             =item * PUTE
146              
147             =item * RANGE
148              
149             =item * UPORDOWN
150              
151             =back
152              
153             =cut
154              
155             has bet_type => (
156             is => 'ro',
157             isa => 'Str',
158             required => 1,
159             );
160              
161             =head2 currency
162              
163             (required) The currency of the payout for this contract, e.g. C<USD>.
164              
165             =cut
166              
167             has currency => (
168             is => 'ro',
169             isa => 'Str',
170             required => 1,
171             );
172              
173             =head2 date_expiry
174              
175             (optional) When the contract expires.
176              
177             One of C<date_expiry> or L</duration> must be provided.
178              
179             =cut
180              
181             has date_expiry => (
182             is => 'rw',
183             @date_attribute,
184             );
185              
186             =head2 date_pricing
187              
188             (optional) The date at which we're pricing the contract. Provide C< undef > to indicate "now".
189              
190             =cut
191              
192             has date_pricing => (
193             is => 'ro',
194             @date_attribute,
195             );
196              
197             =head2 date_start
198              
199             For American contracts, defines when the contract starts.
200              
201             For Europeans, this is used to determine the barrier when the requested barrier is relative.
202              
203             =cut
204              
205             has date_start => (
206             is => 'ro',
207             @date_attribute,
208             );
209              
210             =head2 duration
211              
212             (optional) The requested contract duration, specified as a string indicating value with units.
213             The unit is provided as a single character suffix:
214              
215             =over 4
216              
217             =item * t - ticks
218              
219             =item * s - seconds
220              
221             =item * m - minutes
222              
223             =item * h - hours
224              
225             =item * d - days
226              
227             =back
228              
229             Examples would be C< 5t > for 5 ticks, C< 3h > for 3 hours.
230              
231             One of L</date_expiry> or C<duration> must be provided.
232              
233             =cut
234              
235             has duration => (is => 'ro');
236              
237             =head2 is_forward_starting
238              
239             True if this contract is considered as forward-starting at L</date_pricing>.
240              
241             =cut
242              
243             # TODO This should be a method, but will cause test failures since there are
244             # places where we set this explicitly.
245             has is_forward_starting => (
246             is => 'ro',
247             lazy_build => 1,
248             );
249              
250             =head2 payout
251              
252             Payout amount value, see L</currency>. Optional - only applies to binaries.
253              
254             =cut
255              
256             has payout => (
257             is => 'ro',
258             isa => 'Num',
259             lazy_build => 1,
260             );
261              
262             =head2 prediction
263              
264             Prediction (for tick trades) is what client predicted would happen.
265              
266             =cut
267              
268             has prediction => (
269             is => 'ro',
270             isa => 'Maybe[Num]',
271             );
272              
273             =head2 starts_as_forward_starting
274              
275             This attribute tells us if this contract was initially bought as a forward starting contract.
276             This should not be mistaken for L</is_forward_starting> attribute as that could change over time.
277              
278             =cut
279              
280             has starts_as_forward_starting => (
281             is => 'ro',
282             default => 0,
283             );
284              
285             =head2 pip_size
286              
287             Barrier pip size the minimum fluctuation amount for type of market. It is normally fraction.
288              
289             =cut
290              
291             has pip_size => (
292             is => 'ro',
293             isa => 'Num',
294             lazy_build => 1
295             );
296              
297             =head2 absolute_barrier_multiplier
298              
299             True if barrier multiplier should be applied for absolute barrier(s) on this market
300              
301             =cut
302              
303             has 'absolute_barrier_multiplier' => (
304             is => 'ro',
305             isa => 'Bool',
306             );
307              
308             =head2 supplied_barrier_type
309              
310             One of:
311              
312             =over 4
313              
314             =item * C<relative> - this is of the form C<< S10P >> or C<< S-4P >>, which would be 10 pips above the spot
315             or 4 pips below the spot.
316              
317             =item * C<absolute> - this is a number that can be compared directly with the spot, e.g. C<< 103.45 >>.
318              
319             =item * C<difference> - a numerical difference from the spot, can be negative, e.g. C<< -0.035 >>.
320              
321             =back
322              
323             =cut
324              
325             has supplied_barrier_type => (
326             is => 'ro',
327             );
328              
329             =head2 supplied_high_barrier
330              
331             For a 2-barrier contract, this is the high barrier string. The meaning of these barrier values is controlled by L</supplied_barrier_type>.
332              
333             =head2 supplied_low_barrier
334              
335             For a 2-barrier contract, this is the low barrier string.
336              
337             =head2 supplied_barrier
338              
339             For a single-barrier contract, this is the barrier string.
340              
341             =cut
342              
343             has [qw(supplied_barrier supplied_high_barrier supplied_low_barrier)] => (is => 'ro');
344              
345             =head2 tick_count
346              
347             Number of ticks in this trade.
348              
349             =cut
350              
351             has tick_count => (
352             is => 'ro',
353             isa => 'Maybe[Num]',
354             );
355              
356             has remaining_time => (
357             is => 'ro',
358             isa => 'Time::Duration::Concise',
359             lazy_build => 1,
360             );
361              
362             =head2 underlying_symbol
363              
364             The underlying asset, as a string (for example, C< frxUSDJPY >).
365              
366             =cut
367              
368             has xxx_underlying_symbol => (
369             is => 'ro',
370             isa => 'Str',
371             );
372              
373             # This is needed to determine if a contract is newly priced
374             # or it is repriced from an existing contract.
375             # Milliseconds matters since UI is reacting much faster now.
376             has _date_pricing_milliseconds => (
377             is => 'rw',
378             );
379              
380             =head1 ATTRIBUTES - From contract_types.yml
381              
382             =head2 id
383              
384             A unique numeric ID.
385              
386             =head2 pricing_code
387              
388             Used to determine the pricing engine that should be used for this contract. Examples
389             include 'PUT' or 'CALL'.
390              
391             =head2 display_name
392              
393             This is a human-readable name for the contract type, brief description of what it does.
394              
395             =head2 sentiment
396              
397             Indicates whether we are speculating on market rise or fall.
398              
399             =head2 other_side_code
400              
401             Opposite type for this contract - PUT for CALL, etc.
402              
403             =head2 payout_type
404              
405             Either C< binary > or C< non-binary >.
406              
407             =head2 payouttime
408              
409             Indicates when the contract pays out. Can be C< end > or C< hit >.
410              
411             =cut
412              
413             has [qw(id pricing_code display_name sentiment other_side_code payout_type payouttime)] => (
414             is => 'ro',
415             default => undef,
416             );
417              
418             =head1 ATTRIBUTES - From contract_categories.yml
419              
420             =cut
421              
422             subtype 'contract_category', as 'Finance::Contract::Category';
423             coerce 'contract_category', from 'Str', via { Finance::Contract::Category->new($_) };
424              
425             has category => (
426             is => 'ro',
427             isa => 'contract_category',
428             coerce => 1,
429             handles => [qw(
430             allow_forward_starting
431             barrier_at_start
432             is_path_dependent
433             supported_expiries
434             two_barriers
435             )
436             ],
437             );
438              
439             =head2 allow_forward_starting
440              
441             True if we allow forward starting for this contract type.
442              
443             =cut
444              
445             =head2 barrier_at_start
446              
447             Boolean which will false if we don't know what the barrier is at the start of the contract (Asian contracts).
448              
449             =cut
450              
451             =head2 category_code
452              
453             The code for this category.
454              
455             =cut
456              
457             sub category_code {
458             my $self = shift;
459             return $self->category->code;
460             }
461              
462             =head2 is_path_dependent
463              
464             True if this is a path-dependent contract.
465              
466             =cut
467              
468             =head2 supported_expiries
469              
470             Which expiry durations we allow for this category. Values can be:
471              
472             =over 4
473              
474             =item * intraday
475              
476             =item * daily
477              
478             =item * tick
479              
480             =back
481              
482             =cut
483              
484             =head2 two_barriers
485              
486             True if the contract has two barriers.
487              
488             =cut
489              
490             =head1 METHODS
491              
492             =cut
493              
494             our $BARRIER_CATEGORIES = {
495             callput => ['euro_atm', 'euro_non_atm'],
496             endsinout => ['euro_non_atm'],
497             touchnotouch => ['american'],
498             staysinout => ['american'],
499             digits => ['non_financial'],
500             asian => ['asian'],
501             };
502              
503             =head2 barrier_category
504              
505             Type of barriers we have for this contract, depends on the contract type.
506              
507             Possible values are:
508              
509             =over 4
510              
511             =item * C<american> - barrier for American-style contract
512              
513             =item * C<asian> - Asian-style contract
514              
515             =item * C<euro_atm> - at-the-money European contract
516              
517             =item * C<euro_non_atm> - non-at-the-money European contract
518              
519             =item * C<non_financial> - digits
520              
521             =back
522              
523             =cut
524              
525             sub barrier_category {
526             my $self = shift;
527              
528             my $barrier_category;
529             if ($self->category->code eq 'callput') {
530             $barrier_category = ($self->is_atm_bet) ? 'euro_atm' : 'euro_non_atm';
531             } else {
532             $barrier_category = $BARRIER_CATEGORIES->{$self->category->code}->[0];
533             }
534              
535             return $barrier_category;
536             }
537              
538             =head2 effective_start
539              
540             =over 4
541              
542             =item * For backpricing, this is L</date_start>.
543              
544             =item * For a forward-starting contract, this is L</date_start>.
545              
546             =item * For all other states - i.e. active, non-expired contracts - this is L</date_pricing>.
547              
548             =back
549              
550             =cut
551              
552             sub effective_start {
553             my $self = shift;
554              
555             return
556             ($self->date_pricing->epoch >= $self->date_expiry->epoch) ? $self->date_start
557             : ($self->date_pricing->is_after($self->date_start)) ? $self->date_pricing
558             : $self->date_start;
559             }
560              
561             =head2 fixed_expiry
562              
563             A Boolean to determine if this bet has fixed or flexible expiries.
564              
565             =cut
566              
567             has fixed_expiry => (
568             is => 'ro',
569             default => 0,
570             );
571              
572             =head2 get_time_to_expiry
573              
574             Returns a TimeInterval to expiry of the bet. For a forward start bet, it will NOT return the bet lifetime, but the time till the bet expires.
575              
576             If you want to get the contract life time, use:
577              
578             $contract->get_time_to_expiry({from => $contract->date_start})
579              
580             =cut
581              
582             sub get_time_to_expiry {
583             my ($self, $attributes) = @_;
584              
585             $attributes->{'to'} = $self->date_expiry;
586              
587             return $self->_get_time_to_end($attributes);
588             }
589              
590             =head2 is_after_expiry
591              
592             Returns true if the contract is already past the expiry time.
593              
594             =cut
595              
596             sub is_after_expiry {
597             my $self = shift;
598              
599             die "Not supported for tick expiry contracts" if $self->tick_expiry;
600              
601             return ($self->get_time_to_expiry->seconds == 0) ? 1 : 0;
602             }
603              
604             =head2 is_atm_bet
605              
606             Is this contract meant to be ATM or non ATM at start?
607             The status will not change throughout the lifetime of the contract due to differences in offerings for ATM and non ATM contracts.
608              
609             =cut
610              
611             sub is_atm_bet {
612             my $self = shift;
613              
614             return 0 if $self->two_barriers;
615             # if not defined, it is non ATM
616             return 0 if not defined $self->supplied_barrier;
617             return 0 if $self->supplied_barrier ne 'S0P';
618             return 1;
619             }
620              
621             =head2 shortcode
622              
623             This is a compact string representation of a L<Finance::Contract> object. It includes all data needed to
624             reconstruct a contract, with the exception of L</currency>.
625              
626             =cut
627              
628             sub shortcode {
629             my $self = shift;
630              
631             my $shortcode_date_start = (
632             $self->is_forward_starting
633             or $self->starts_as_forward_starting
634             ) ? $self->date_start->epoch . 'F' : $self->date_start->epoch;
635             my $shortcode_date_expiry =
636             ($self->tick_expiry) ? $self->tick_count . 'T'
637             : ($self->fixed_expiry) ? $self->date_expiry->epoch . 'F'
638             : $self->date_expiry->epoch;
639              
640             # TODO We expect to have a valid bet_type, but there may be codepaths which don't set this correctly yet.
641             my $contract_type = $self->bet_type // $self->code;
642             my @shortcode_elements = ($contract_type, $self->underlying->symbol, $self->payout, $shortcode_date_start, $shortcode_date_expiry);
643              
644             if ($self->two_barriers) {
645             push @shortcode_elements, map { $self->_barrier_for_shortcode_string($_) } ($self->supplied_high_barrier, $self->supplied_low_barrier);
646             } elsif (defined $self->supplied_barrier and $self->barrier_at_start) {
647             push @shortcode_elements, ($self->_barrier_for_shortcode_string($self->supplied_barrier), 0);
648             }
649              
650             return uc join '_', @shortcode_elements;
651             }
652              
653             =head2 tick_expiry
654              
655             A boolean that indicates if a contract expires after a pre-specified number of ticks.
656              
657             =cut
658              
659             sub tick_expiry {
660             my ($self) = @_;
661             return $self->tick_count ? 1 : 0;
662             }
663              
664             =head2 timeinyears
665              
666             Contract duration in years.
667              
668             =head2 timeindays
669              
670             Contract duration in days.
671              
672             =cut
673              
674             has [qw(
675             timeinyears
676             timeindays
677             )
678             ] => (
679             is => 'ro',
680             init_arg => undef,
681             isa => 'Math::Util::CalculatedValue::Validatable',
682             lazy_build => 1,
683             );
684              
685             sub _build_timeinyears {
686             my $self = shift;
687              
688             my $tiy = Math::Util::CalculatedValue::Validatable->new({
689             name => 'time_in_years',
690             description => 'Bet duration in years',
691             set_by => 'Finance::Contract',
692             base_amount => 0,
693             minimum => 0.000000001,
694             });
695              
696             my $days_per_year = Math::Util::CalculatedValue::Validatable->new({
697             name => 'days_per_year',
698             description => 'We use a 365 day year.',
699             set_by => 'Finance::Contract',
700             base_amount => 365,
701             });
702              
703             $tiy->include_adjustment('add', $self->timeindays);
704             $tiy->include_adjustment('divide', $days_per_year);
705              
706             return $tiy;
707             }
708              
709             sub _build_timeindays {
710             my $self = shift;
711              
712             my $atid = $self->get_time_to_expiry({
713             from => $self->effective_start,
714             })->days;
715              
716             my $tid = Math::Util::CalculatedValue::Validatable->new({
717             name => 'time_in_days',
718             description => 'Duration of this bet in days',
719             set_by => 'Finance::Contract',
720             minimum => 0.000001,
721             maximum => 730,
722             base_amount => $atid,
723             });
724              
725             return $tid;
726             }
727              
728             # INTERNAL METHODS
729              
730             # Send in the correct 'to'
731             sub _get_time_to_end {
732             my ($self, $attributes) = @_;
733              
734             my $end_point = $attributes->{to};
735             my $from = ($attributes and $attributes->{from}) ? $attributes->{from} : $self->date_pricing;
736              
737             # Don't worry about how long past expiry
738             # Let it die if they gave us nonsense.
739              
740             return Time::Duration::Concise->new(
741             interval => max(0, $end_point->epoch - $from->epoch),
742             );
743             }
744              
745             #== BUILDERS =====================
746              
747             sub _build_date_pricing {
748             return Date::Utility->new;
749             }
750              
751             sub _build_is_forward_starting {
752             my $self = shift;
753              
754             return ($self->allow_forward_starting and $self->date_pricing->is_before($self->date_start)) ? 1 : 0;
755             }
756              
757             sub _build_remaining_time {
758             my $self = shift;
759              
760             my $when = ($self->date_pricing->is_after($self->date_start)) ? $self->date_pricing : $self->date_start;
761              
762             return $self->get_time_to_expiry({
763             from => $when,
764             });
765             }
766              
767             sub _build_date_start {
768             return Date::Utility->new;
769             }
770              
771             # Generates a string version of a barrier by multiplying the actual barrier to remove the decimal point
772             sub _barrier_for_shortcode_string {
773             my ($self, $string) = @_;
774              
775             return $string if $self->supplied_barrier_type eq 'relative';
776              
777             # better to use sprintf else roundcommon can return as 1e-1 which will be concatenated as it is
778             return 'S' . sprintf('%0.0f', roundcommon(1, $string / $self->pip_size)) . 'P' if $self->supplied_barrier_type eq 'difference';
779              
780             $string = $self->_pipsized_value($string);
781             if ($self->bet_type !~ /^DIGIT/ && $self->absolute_barrier_multiplier) {
782             $string *= _FOREX_BARRIER_MULTIPLIER;
783             } else {
784             $string = floor($string);
785             }
786              
787             # Make sure it's rounded to an integer and returned as string
788             # as sub definition states generates a string
789             $string = sprintf('%0.0f', roundcommon(1, $string));
790              
791             return $string;
792             }
793              
794             sub _pipsized_value {
795             my ($self, $value) = @_;
796              
797             my $display_decimals = log(1 / $self->pip_size) / log(10);
798             $value = sprintf '%.' . $display_decimals . 'f', $value;
799             return $value;
800             }
801              
802             no Moose;
803             __PACKAGE__->meta->make_immutable;
804              
805             1;