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

use strict;
use warnings;
use Carp;
use constant {
    LONG  => 7,
    SHORT => 9,
    HP    => 1,
    LP    => 0,
};

1;

sub tag { (shift)->{tag} }

sub recommended {
    my $class = shift;
       $class->new(0.02, 0.20);
}

sub new {
    my $class = shift;
    my $this  = bless {e=>[], y=>[]}, $class;

    if( @_ ) {
       eval { $this->set_alpha(@_) };
       croak $@ if $@;
    }

    return $this;
}

sub set_alpha {
    my $this = shift;
    my ($as,$am) = @_;

    croak "set_alpha(as,am) takes two arguments, the alpha start (0<as<1) and the alpha max (0<as<am<1)"
        unless 0 < $as and $as < $am and $am < 1;

    $this->{as} = $as;
    $this->{am} = $am;

    $this->{tag} = "PSAR($as,$am)";

    return;
}

sub insert {
    my $this = shift;

    my ($as,$am);
    croak "must set_alpha(as,am) before inserting data" unless defined( $am = $this->{am} ) and defined( $as = $this->{as} );

    my ($y_low, $y_high) = @{$this->{y}};
    my ($open,$high,$low,$close);

    my $S;
    my $P = $this->{S};
    my $A = $this->{A};
    my $e = $this->{e};

    my $ls = $this->{ls};

    while( defined( my $ar = shift ) ) {
        croak "arguments to insert must be four tuple (open,high,low,close) with high greater than or equal to low"
            unless ref($ar) eq "ARRAY" and @$ar==4 and $ar->[2]<=$ar->[1];

        # NOTE: we really only use open and close to initialize ...
        ($open,$high,$low,$close) = @$ar;

        if( defined $ls ) {
            # calculate sar_t
            # The Encyclopedia of Technical Market Indicators - Page 495

            my @oe = @$e;
            $e->[HP] = $high if $high > $e->[HP]; # the highest point during the trend
            $e->[LP] = $low  if $low  < $e->[LP]; # the  lowest point during the trend

            if( $ls == LONG ) {
                $S = $P + $A*($e->[HP] - $P); # adjusted upwards from the reset like so

                # NOTE: many sources say you should flop short/long if you get
                # inside the price range for the last *two* periods.  Amazon,
                # Yahoo! and stockcharts dont' seem to do it that way.

                if( $S > $low ) {
                    $ls = SHORT; # new short position

                    $S  = $e->[HP];
                    $A  = $as;

                    $e->[HP] = ($high>$y_high ? $high : $y_high);
                    $e->[LP] = ($low <$y_low  ? $low  : $y_low );

                } elsif( $S > $y_low ) {
                    $S = $y_low;

                } elsif( $oe[HP] != $e->[HP] ) {
                    $A += $as;
                    $A = $am if $A > $am;
                }

            } else {
                $S = $P + $A*($e->[LP] - $P); # adjusted downwards from the reset like so

                # NOTE: many sources say you should flop short/long if you get
                # inside the price range for the last *two* periods.  Amazon,
                # Yahoo! and stockcharts dont' seem to do it that way.

                if( $S < $high ) {
                    $ls = LONG; # new long position

                    $S  = $e->[LP];
                    $A  = $as;

                    $e->[HP] = ($high>$y_high ? $high : $y_high);
                    $e->[LP] = ($low <$y_low  ? $low  : $y_low );

                } elsif( $S < $y_high ) {
                    $S = $y_high;

                } elsif( $oe[LP] != $e->[LP] ) {
                    $A += $as;
                    $A = $am if $A > $am;
                }
            }

        } else {
            # circa 2010: initialize somehow
            # (never did find a good description of how to initialize this mess.
            #   I think you're supposed to tell it how to start)
            # this is the only time we use open/close and it's not even in the definition
            #
            # 2011-01-03: I did look this up, it's the "SIP" or significant
            # point.  It should be the lowest (or the highest) point we have
            # from our recent-ish data or "long trade" as he calls it.  This'll
            # do as an approximation of that imo — otherwise we'll have to
            # start asking for a few days previous trades just to initialize.

            $A = $as;

            if( $open < $close ) {
                $ls = LONG;
                $S  = $low;

            } else {
                $ls = SHORT;
                $S  = $high;
            }

            $e->[HP] = $high;
            $e->[LP] = $low;
        }

        $P = $S;

        ($y_low, $y_high) = ($low, $high);
    }

    ## DEBUG ## warn "{S}=$S; {A}=$A";

    $this->{S}  = $S;
    $this->{A}  = $A;
    $this->{ls} = $ls;

    @{$this->{y}} = ($y_low, $y_high);
}

sub query {
    my $this = shift;

    $this->{S};
}

sub long {
    my $this = shift;
    $this->{ls} == LONG;
}

sub short {
    my $this = shift;
    $this->{ls} == SHORT;
}

__END__

=encoding utf-8

=head1 NAME

Math::Business::ParabolicSAR - Technical Analysis: Stop and Reversal (aka SAR)

=head1 SYNOPSIS

  use Math::Business::ParabolicSAR;

  my $sar = new Math::Business::ParabolicSAR;
     $sar->set_alpha(0.02, 0.2);

  # alternatively/equivilently
  my $sar = new Math::Business::ParabolicSAR(0.02, 0.2);

  # or to just get the recommended model ... (0.02, 0.2)
  my $sar = Math::Business::ParabolicSAR->recommended;

  my @data_points = (
      ["35.0300", "35.1300", "34.3600", "34.3900"],
      ["34.6400", "35.0000", "34.2100", "34.7400"],
      ["34.6900", "35.1400", "34.3800", "34.7900"],
      ["35.2900", "35.7900", "35.0800", "35.5200"],
      ["35.9000", "36.0600", "35.7500", "36.0600"],
      ["36.1300", "36.7200", "36.0500", "36.5800"],
      ["36.4100", "36.6400", "36.2600", "36.6100"],
      ["36.3500", "36.5500", "35.9400", "35.9700"],
  );

  # choose one:
  $sar->insert( @data_points );
  $sar->insert( $_ ) for @data_points;

  my $sar = $sar->query;
  print "SAR: $sar\n";

  # Briefly, the SAR is below the price data when you're meant to
  # be holding stocks (long) and above it when you're meant to be
  # on margin (short).

  print "The SAR is long  today.\n" if $sar->long;
  print "The SAR is short today.\n" if $sar->short;

=head1 RESEARCHER

The Parabolic Stop and Reversal was designed by J. Welles Wilder Jr circa 1978.

The SAR is meant to be used to "stop loss" on a position.  It assumes you always
have a position in the market (long if you're holding stocks and short when
you're on margin).  When the SAR crosses the price data, it signals a sell (when
you're long) or a buy (when you're short).

Wilder himself felt the SAR was particularly vulnerable to "whipsaws" and
recommended only using the SAR when the ADX is above 30 -- that is, when there
is a strong trend going.

=head1 THANKS

Gustav C<< <gustavf@gmail.com> >>

=head1 AUTHOR

Paul Miller C<< <jettero@cpan.org> >>

I am using this software in my own projects...  If you find bugs, please please
please let me know.  There is a mailing list with very light traffic that you
might want to join: L<http://groups.google.com/group/stockmonkey/>.

=head1 COPYRIGHT

Copyright © 2013 Paul Miller

=head1 LICENSE

This is released under the Artistic License. See L<perlartistic>.

=head1 SEE ALSO

perl(1), L<Math::Business::StockMonkey>, L<Math::Business::StockMonkey::FAQ>, L<Math::Business::StockMonkey::CookBook>

L<http://en.wikipedia.org/wiki/Parabolic_SAR>

The Encyclopedia of Technical Market Indicators - Page 495

=cut