The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Finance::Instrument;

use strict;
use 5.008_001;
our $VERSION = '0.02';

use Moose;
use methods;
use Finance::Instrument::Domain;
use Finance::Instrument::Exchange;
use MooseX::ClassAttribute;
use DateTime;

class_has global => (is => "rw", isa => "Financial::Instrument::Domain",
                     default => sub { Finance::Instrument::Domain->global },
                     handles => [qw(load_default_exchanges
                                    load_instrument_from_yml
                                    load_default_instrument
                                    get_exchange
                                    add_exchange
                                    get
                                    load_instrument
                               )]);

has domain => (is => "ro", isa => "Finance::Instrument::Domain", weak_ref => 1,
               default => sub { Finance::Instrument::Domain->global });

has code => (is => "ro", isa => "Str");
has name => (is => "ro", isa => "Str");

has attributes => (is => "rw", isa => "HashRef",
                   traits  => ['Hash'],
                   default => sub { {} },
                   handles   => {
                       attr     => 'accessor'
                   });

has time_zone => (is => "rw", isa => "Str");
has tick_size => (is => "rw", isa => "Num");
has exchange  => (is => "rw", isa => "Finance::Instrument::Exchange");
has session   => (is => "rw", isa => "ArrayRef[ArrayRef[Int]]");

has override_since => (is => "rw", isa => "HashRef", default => sub { {} });

has override_dow   => (is => "rw", isa => "HashRef", default => sub { {} });

method BUILD {
    $self->domain->add($self);
}

around attr => sub {
    my ($next, $self, @args) = @_;
    my ($key, $val) = @args;
    my $ret = $self->$next(@args);
    unless (defined $ret || defined $val) {
        $ret = $self->exchange->attr($key)
    }
    return $ret;
};

sub get_spec_for_date {
    my ($self, $date) = @_;
    my $spec = { map { $_ => $self->$_ } qw(time_zone session tick_size override_dow) };
    for (sort keys %{$self->override_since}) {
        last if $_ gt $date;
        $spec = { %$spec, %{$self->override_since->{$_}} };
    }
    if ($self->{override_dow}) {
        my ($y, $m, $d) = split('-', $date);
        my $dt = DateTime->new( year => $y, month => $m, day => $d,
                                time_zone => $spec->{time_zone} || $self->time_zone );
        $spec = { %$spec, %{$spec->{override_dow}{$dt->day_of_week} || {}} };
    }
    return $spec;
}

method derive_session($_dt) {
    my $dt = $_dt->clone->truncate(to => 'day');
    my $session = $self->session_for_day($dt);

    if ($session->[-1][1] > 1440 && $_dt <= $dt->clone->add(minutes => $session->[-1][1] - 1440)) {
        my $prev_session = $self->session_for_day($dt->add(days => -1));
        if ($_dt <=  $dt->clone->add(minutes => $session->[-1][1] - 1440)) {
            return ($prev_session, $dt, $#{$prev_session});
        }
    }

    my $idx = 0;
    for (@{ $session }) {
        my ($start, $end) = map { $dt->clone->add(minutes => $_) } @$_;
        if ($_dt <= $end) {
            last;
        }
        ++$idx;
    }

    if ($idx >= @{ $session }) {
        $idx = 0;
        $dt->add(days => 1);
        return $self->derive_session($dt);
    }

    return ($session, $dt, $idx);
}

method session_for_day($dt) {
    $self->get_spec_for_date($dt->ymd)->{session} || $self->session;
}

method trading_time_for_day($dt) {
    my ($session) = $self->session_for_day($dt);
    my $base = $dt->clone->truncate(to => 'day')->epoch;
    return [map { [ $base + $_->[0] * 60, $base + $_->[1] * 60 ] } @$session]
}

__PACKAGE__->meta->make_immutable;
no Moose;
1;

__END__

=encoding utf-8

=for stopwords

=head1 NAME

Finance::Instrument - Object representation of financial instruments

=head1 SYNOPSIS

  use Finance::Instrument;
  # load all iso10383 market identification exchanges
  Finance::Instrument->load_default_exchanges;
  Finance::Instrument->get_exchange('XHKF');

  my $spec = { type => 'Futures',
               exchange => 'XHKF',
               code => 'HSI',
               time_zone => 'Asia/Hong_Kong',
               currency => 'HKD',
               session => [[555, 720], [810, 975]] };

  my $hsi = Finance::Instrument->load_instrument($spec);
  my $contract = $hsi->contract(2012, 2);
  $contract->trading_time_for_day(DateTime->now);

=head1 DESCRIPTION

Finance::Instrument models financial instruments and provide utility
functions, such as calculating current or next trading hours for a
given L<DateTime>, or retrieve near-term futures contract from futures
contract series.

=head1 AUTHOR

Chia-liang Kao E<lt>clkao@clkao.orgE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 SEE ALSO

=cut