Finance-Alpaca

 view release on metacpan or  search on metacpan

lib/Finance/Alpaca.pm  view on Meta::CPAN

package Finance::Alpaca 0.9906 {
    use strictures 2;
    use Moo;
    use feature 'signatures';
    no warnings 'experimental::signatures';
    use Mojo::UserAgent;
    use Types::Standard qw[ArrayRef Bool Dict Enum InstanceOf Maybe Num Str Int];
    use Types::UUID;
    #
    use lib '../../lib/';
    use Finance::Alpaca::DataStream;
    use Finance::Alpaca::Struct::Account qw[to_Account];
    use Finance::Alpaca::Struct::Activity qw[to_Activity Activity];
    use Finance::Alpaca::Struct::Asset qw[to_Asset Asset];
    use Finance::Alpaca::Struct::Bar qw[to_Bar Bar];
    use Finance::Alpaca::Struct::Calendar qw[to_Calendar Calendar];
    use Finance::Alpaca::Struct::Configuration qw[to_Configuration Configuration];
    use Finance::Alpaca::Struct::Clock qw[to_Clock];
    use Finance::Alpaca::Struct::Order qw[to_Order Order];
    use Finance::Alpaca::Struct::Position qw[to_Position Position];
    use Finance::Alpaca::Struct::Quote qw[to_Quote Quote];
    use Finance::Alpaca::Struct::Trade qw[to_Trade Trade];
    use Finance::Alpaca::Struct::TradeActivity qw[to_TradeActivity TradeActivity];
    use Finance::Alpaca::Struct::Watchlist qw[to_Watchlist Watchlist];
    use Finance::Alpaca::TradeStream;
    use Finance::Alpaca::Types;
    #
    has ua => ( is => 'lazy', isa => InstanceOf ['Mojo::UserAgent'] );

    sub _build_ua ($s) {
        my $ua = Mojo::UserAgent->new;
        $ua->transactor->name(
            sprintf 'Finance::Alpaca %f (Perl %s)',
            $Finance::Alpaca::VERSION, $^V
        );
        $ua->on(
            start => sub ( $ua, $tx ) {
                $tx->req->headers->header( 'APCA-API-KEY-ID'     => $s->keys->[0] ) if $s->has_keys;
                $tx->req->headers->header( 'APCA-API-SECRET-KEY' => $s->keys->[1] ) if $s->has_keys;
            }
        );
        return $ua;
    }
    has api_version => ( is => 'ro', isa => Enum [ 1, 2 ], required => 1, default => 2 );
    has paper => ( is => 'rw', isa => Bool, required => 1, default => 0, coerce => 1 );

    sub endpoint ($s) {
        $s->paper ? 'https://paper-api.alpaca.markets' : '';
    }
    has keys => ( is => 'rwp', isa => ArrayRef [ Str, 2 ], predicate => 1 );
    #
    sub account ($s) {
        my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/account' );
        $tx = $s->ua->start($tx);
        return to_Account( $tx->result->json );
    }

    sub clock ($s) {
        my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/clock' );
        $tx = $s->ua->start($tx);
        return to_Clock( $tx->result->json );
    }

    sub calendar ( $s, %params ) {
        my $params = '';
        $params .= '?' . join '&', map {
            $_ . '='
                . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
        } keys %params if keys %params;
        my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/calendar' . $params );
        $tx = $s->ua->start($tx);
        return @{ ( ArrayRef [Calendar] )->assert_coerce( $tx->result->json ) };
    }

    sub assets ( $s, %params ) {
        my $params = '';
        $params .= '?' . join '&', map {
            $_ . '='
                . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
        } keys %params if keys %params;
        return @{
            ( ArrayRef [Asset] )->assert_coerce(
                $s->ua->get( $s->endpoint . '/v2/assets' . $params )->result->json
            )
        };

    }

    sub asset ( $s, $symbol_or_asset_id ) {
        my $res = $s->ua->get( $s->endpoint . '/v2/assets/' . $symbol_or_asset_id )->result;
        return $res->is_error ? () : to_Asset( $res->json );
    }

    sub bars ( $s, %params ) {
        my $symbol = delete $params{symbol};
        my $params = '';
        $params .= '?' . join '&', map {
            $_ . '='
                . (
                ref $params{$_} eq 'Time::Moment'
                ? $params{$_}->strftime('%Y-%m-%dT%H:%M:%S%Z')
                : $params{$_}
                )
        } keys %params if keys %params;
        my $res = $s->ua->get(
            sprintf 'https://data.alpaca.markets/v%d/stocks/%s/bars%s',
            $s->api_version, $symbol, $params
        )->result;
        return $res->is_error ? $res->json : (
            ( next_page_token => $res->json->{next_page_token} ),
            map { delete $_->{symbol} => delete $_->{bars} }
                ( Dict [ bars => ArrayRef [Bar], symbol => Str, next_page_token => Maybe [Str] ] )
                ->assert_coerce( $res->json )
        );
    }

    sub quotes ( $s, %params ) {
        my $symbol = delete $params{symbol};
        my $params = '';
        $params .= '?' . join '&', map {
            $_ . '='
                . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
        } keys %params if keys %params;
        my $res = $s->ua->get(
            sprintf 'https://data.alpaca.markets/v%d/stocks/%s/quotes%s',
            $s->api_version, $symbol, $params
        )->result;
        return $res->is_error ? $res->json : (
            ( next_page_token => $res->json->{next_page_token} ),
            map { delete $_->{symbol} => delete $_->{quotes} } (
                Dict [ quotes => ArrayRef [Quote], symbol => Str, next_page_token => Maybe [Str] ]
            )->assert_coerce( $res->json )
        );
    }

    sub trades ( $s, %params ) {
        my $symbol = delete $params{symbol};
        for ( keys %params ) {
            $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
        }
        my $res = $s->ua->get(
            sprintf(
                'https://data.alpaca.markets/v%d/stocks/%s/trades',
                $s->api_version, $symbol
            ) => form => {%params}
        )->result;
        return $res->is_error ? $res->json : (
            ( next_page_token => $res->json->{next_page_token} ),
            map { delete $_->{symbol} => delete $_->{trades} } (
                Dict [ trades => ArrayRef [Trade], symbol => Str, next_page_token => Maybe [Str] ]
            )->assert_coerce( $res->json )
        );
    }

    sub trade_stream ( $s, $cb, %params ) {
        my $stream = Finance::Alpaca::TradeStream->new( cb => $cb );
        $stream->authorize( $s->ua, $s->keys, $s->paper )->catch(
            sub ($err) {
                $stream = ();
                warn "WebSocket error: $err";
            }
        )->wait;
        $stream;
    }

    sub data_stream ( $s, $cb, %params ) {
        my $stream = Finance::Alpaca::DataStream->new(
            cb     => $cb,
            source => delete $params{source} // 'iex'    # iex or sip
        );
        $stream->authorize( $s->ua, $s->keys )->catch(
            sub ($err) {
                $stream = ();
                warn "WebSocket error: $err";
            }
        )->wait;
        $stream;
    }

    sub orders ( $s, %params ) {
        for ( keys %params ) {
            $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
        }
        return @{
            ( ArrayRef [Order] )->assert_coerce(
                $s->ua->get( $s->endpoint . '/v2/orders' => form => {%params} )->result->json
            )
        };
    }

    sub order_by_id ( $s, $order_id, $nested = 0 ) {
        my $res
            = $s->ua->get(
            $s->endpoint . '/v2/orders/' . $order_id => form => ( $nested ? { nested => 1 } : () ) )
            ->result;
        return $res->is_error ? () : to_Order( $res->json );
    }

    sub order_by_client_id ( $s, $order_id ) {
        my $res
            = $s->ua->get( $s->endpoint
                . '/v2/orders:by_client_order_id' => form => { client_order_id => $order_id } )
            ->result;
        return $res->is_error ? () : to_Order( $res->json );
    }

    sub create_order ( $s, %params ) {
        $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
            if defined $params{extended_hours};
        my $res = $s->ua->post( $s->endpoint . '/v2/orders' => json => \%params )->result;
        return $res->is_error ? $res->json : to_Order( $res->json );
    }

    sub replace_order ( $s, $order_id, %params ) {
        $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
            if defined $params{extended_hours};
        my $res
            = $s->ua->patch( $s->endpoint . '/v2/orders/' . $order_id => json => \%params )->result;
        return $res->is_error ? $res->json : to_Order( $res->json );
    }

    sub cancel_orders ($s) {
        my $res = $s->ua->delete( $s->endpoint . '/v2/orders' )->result;
        return $res->is_error
            ? $res->json
            : ( ArrayRef [ Dict [ body => Order, id => Uuid, status => Int ] ] )
            ->assert_coerce( $res->json );
    }

    sub cancel_order ( $s, $order_id ) {
        my $res = $s->ua->delete( $s->endpoint . '/v2/orders/' . $order_id )->result;
        return !$res->is_error;
    }

    sub positions ($s) {
        return
            @{ ( ArrayRef [Position] )
                ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/positions' )->result->json ) };
    }

    sub position ( $s, $symbol_or_asset_id ) {
        my $res = $s->ua->get( $s->endpoint . '/v2/positions/' . $symbol_or_asset_id )->result;
        return $res->is_error ? () : to_Position( $res->json );
    }

    sub close_all_positions ( $s, $cancel_orders = !1 ) {
        my $res
            = $s->ua->delete(
            $s->endpoint . '/v2/positions' . ( $cancel_orders ? '?cancel_orders=true' : '' ) )
            ->result;
        return $res->is_error
            ? $res->json
            : ( ArrayRef [ Dict [ body => Order, id => Uuid, status => Int ] ] )
            ->assert_coerce( $res->json );
    }

    sub close_position ( $s, $symbol_or_asset_id, $qty = () ) {
        my $res
            = $s->ua->get(
            $s->endpoint . '/v2/positions/' . $symbol_or_asset_id . ( $qty ? '?qty=' . $qty : '' ) )
            ->result;
        return $res->is_error ? () : to_Order( $res->json );
    }

    sub portfolio_history ( $s, %params ) {
        $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
            if defined $params{extended_hours};
        $params{date_end}
            = ref $params{date_end} eq 'Time::Moment'
            ? $params{date_end}->strftime('%F')
            : $params{date_end}
            if defined $params{date_end};
        my $res = $s->ua->get( $s->endpoint . '/v2/account/portfolio/history' => json => \%params )
            ->result;
        return $res->is_error ? $res->json : (
            Dict [
                base_value      => Num,
                equity          => ArrayRef [Num],
                profit_loss     => ArrayRef [Num],
                profit_loss_pct => ArrayRef [Num],
                timeframe       => Str,
                timestamp       => ArrayRef [Timestamp]
            ]
        )->assert_coerce( $res->json );
    }

    sub watchlists ($s) {
        return
            @{ ( ArrayRef [Watchlist] )
                ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/watchlists' )->result->json ) };
    }

    sub create_watchlist ( $s, $name, @symbols ) {
        my $res
            = $s->ua->post( $s->endpoint
                . '/v2/watchlists' => json =>
                { name => $name, ( @symbols ? ( symbols => \@symbols ) : () ) } )->result;
        return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
    }

    sub delete_watchlist ( $s, $watchlist_id ) {
        my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
        return $res->is_error ? $res->json : 1;
    }

    sub watchlist ( $s, $watchlist_id ) {
        my $res = $s->ua->get( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
        return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
    }

    sub update_watchlist ( $s, $watchlist_id, %params ) {
        my $res
            = $s->ua->put( $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => {%params} )
            ->result;
        return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
    }

    sub add_to_watchlist ( $s, $watchlist_id, $symbol ) {
        my $res
            = $s->ua->post(
            $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => { symbol => $symbol } )
            ->result;
        return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
    }

    sub remove_from_watchlist ( $s, $watchlist_id, $symbol ) {
        my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id . '/' . $symbol )
            ->result;
        return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
    }

    sub configuration ($s) {
        my $res = $s->ua->get( $s->endpoint . '/v2/account/configurations' )->result;
        return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
    }

    sub modify_configuration ( $s, %params ) {
        my $res = $s->ua->patch( $s->endpoint . '/v2/account/configurations' => json => {%params} )
            ->result;
        return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
    }

    sub activities ( $s, %params ) {
        $params{activity_types} = join ',', @{ $params{activity_types} } if $params{activity_types};
        my $params = '';
        $params .= '?' . join '&', map {
            $_ . '='
                . (
                  ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string()
                : ref $params{$_} eq 'ARRAY'        ? @{ $params{$_} }
                :                                     $params{$_}
                )
        } keys %params if keys %params;
        my $res = $s->ua->get(
            sprintf $s->endpoint . '/v2/account/activities%s',
            $params ? $params : ''
        )->result;
        return $res->is_error
            ? $res->json
            : map { $_->{activity_type} eq 'FILL' ? to_TradeActivity($_) : to_Activity($_) }
            @{ $res->json };
    }
}
1;
__END__

=encoding utf-8

=head1 NAME

Finance::Alpaca - Perl Wrapper for Alpaca's Commission-free Stock Trading API

=head1 SYNOPSIS

    use Finance::Alpaca;
    my $alpaca = Finance::Alpaca->new(
        paper => 1,
        keys  => [ ... ]
    );
    my $order = $alpaca->create_order(
        symbol => 'MSFT',
        qty    => .1,
        side   => 'buy',
        type   => 'market',
        time_in_force => 'day'
    );

=head1 DESCRIPTION

Finance::Alpaca allows you to buy, sell, and short U.S. stocks with zero
commissions with Alpaca, an API first, algo-friendly brokerage.

=head1 METHODS

=head2 C<new( ... )>

    my $camelid = Finance::Alpaca->new(
        keys => [ 'MDJOHHAE5BDE2FAYAEQT',
                  'Xq9p6ovxaa5XKihaEDRgpMapjeWYd5gIM63iq5BL'
                ] );

Creates a new Finance::Alpaca object.

This constructor accepts the following parameters:

=over

=item C<keys> - C<[ $APCA_API_KEY_ID, $APCA_API_SECRET_KEY ]>

Every API call requires authentication. You must provide these keys which may
be acquired in the developer web console and are only visible on creation.

=item C<paper> - Boolean value

If you're attempting to use Alpaca's paper trading system, this B<must> be a
true value. Otherwise, you will be making live trades with real assets!

B<Note>: This is a false value by default.

=back

=head2 C<account( )>

    my $acct = $camelid->account( );
    CORE::say sprintf 'I can%s short!', $acct->shorting_enabled ? '' : 'not';

Returns a L<Finance::Alpaca::Struct::Account> object.

The account endpoint serves important information related to an account,
including account status, funds available for trade, funds available for
withdrawal, and various flags relevant to an account’s ability to trade.

=head2 C<clock( )>

    my $clock = $camelid->clock();
    say sprintf
        $clock->timestamp->strftime('It is %l:%M:%S %p on a %A and the market is %%sopen!'),
        $clock->is_open ? '' : 'not ';

Returns a L<Finance::Alpaca::Struct::Clock> object.

The clock endpoint serves the current market timestamp, whether or not the
market is currently open, as well as the times of the next market open and
close.

=head2 C<calendar( [...] )>

        my @days = $camelid->calendar(
            start => Time::Moment->now,
            end   => Time::Moment->now->plus_days(14)
        );
        for my $day (@days) {
            say sprintf '%s the market opens at %s Eastern',
                $day->date, $day->open;
        }

Returns a list of L<Finance::Alpaca::Struct::Calendar> objects.

The calendar endpoint serves the full list of market days from 1970 to 2029.

The following parameters are accepted:

=over

=item C<start> - The first date to retrieve data for (inclusive); Time::Moment object or RFC3339 string

=item C<end> - The last date to retrieve data for (inclusive); Time::Moment object or RFC3339 string

=back

Both listed parameters are optional. If neither is provided, the calendar will
begin on January 1st, 1970.

=head2 C<assets( [...] )>

    say $_->symbol
        for sort { $a->symbol cmp $b->symbol } @{ $camelid->assets( status => 'active' ) };

Returns a list of L<Finance::Alpaca::Struct::Asset> objects.

The assets endpoint serves as the master list of assets available for trade and
data consumption from Alpaca.

The following parameters are accepted:

=over

=item C<status> - e.g. C<active> or C<inactive>. By default, all statuses are included

=item C<asset_class> - Defaults to C<us_equity>

=back

=head2 C<asset( ... )>

    my $msft = $camelid->asset('MSFT');
    my $spy  = $camelid->asset('b28f4066-5c6d-479b-a2af-85dc1a8f16fb');

Returns a L<Finance::Alpaca::Struct::Asset> object.

You may use either the asset's C<id> (UUID) or C<symbol>. If the asset is not
found, an empty list is returned.

=head2 C<bars( ... )>

  my %bars = $camelid->bars(
        symbol    => 'MSFT',
        timeframe => '1Min',
        start     => Time::Moment->now->with_hour(10),
        end       => Time::Moment->now->minus_minutes(20)
    );

Returns a list of L<Finance::Alpaca::Struct::Bar> objects along with other
data.

The bar endpoint serves aggregate historical data for the requested securities.

The following parameters are accepted:

=over

=item C<symbol> - The symbol to query for; this is required

=item C<start> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<end> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<limit> - Number of data points to return. Must be in range C<1-10000>, defaults to C<1000>

=item C<page_token> - Pagination token to continue from

=item C<timeframe> - Timeframe for the aggregation. Available values are: C<1Min>, C<1Hour>, and C<1Day>; this is required

=back

The method returns a hash reference with bar data included as a list under the
symbol as well as a C<next_page_token> for pagination if applicable.

=head2 C<quotes( ... )>

    my %quotes = $camelid->quotes(
        symbol => 'MSFT',
        start  => Time::Moment->now->with_hour(10),
        end    => Time::Moment->now->minus_minutes(20)
    );

Returns a list of L<Finance::Alpaca::Struct::Quote> objects along with other
data.

The bar endpoint serves quote (NBBO) historical data for the requested
security.

The following parameters are accepted:

=over

=item C<symbol> - The symbol to query for; this is required

=item C<start> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<end> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<limit> - Number of data points to return. Must be in range C<1-10000>, defaults to C<1000>

=item C<page_token> - Pagination token to continue from

=back

The method returns a hash reference with quote data included as a list under
the symbol as well as a C<next_page_token> for pagination if applicable.

=head2 C<trades( ... )>

    my %trades = $camelid->trades(
        symbol => 'MSFT',
        start  => Time::Moment->now->with_hour(10),
        end    => Time::Moment->now->minus_minutes(20)
    );

Returns a list of L<Finance::Alpaca::Struct::Trade> objects along with other
data.

The bar endpoint serves historical trade data for a given ticker symbol on a
specified date.

The following parameters are accepted:

=over

=item C<symbol> - The symbol to query for; this is required

=item C<start> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<end> - Filter data equal to or before this time in RFC-3339 format or a Time::Moment object. Fractions of a second are not accepted; this is required

=item C<limit> - Number of data points to return. Must be in range C<1-10000>, defaults to C<1000>

=item C<page_token> - Pagination token to continue from

=back

The method returns a hash reference with trade data included as a list under
the symbol as well as a C<next_page_token> for pagination if applicable.


=head2 C<trade_stream( ... )>

    my $stream = $camelid->trade_stream( sub ($packet) {  ... } );

Returns a new L<Finance::Alpaca::TradeStream> object.

You are ready to receive real-time account and order data!

This method expects a code reference. This callback will receive all incoming
data.

=head2 C<data_stream( ... )>

    my $stream = $camelid->data_stream( sub ($packet) {  ... } );
    $stream->subscribe(
        trades => ['MSFT']
    );

Returns a new L<Finance::Alpaca::DataStream> object.

You are ready to receive real-time market data!

You can send one or more subscription messages (described in
L<Finance::Alpaca::DataStream>) and after confirmation you will receive the
corresponding market data.

This method expects a code reference. This callback will receive all incoming
data.

=head2 C<orders( [...] )>

    my @orders = $camelid->orders( status => 'open' );

Returns a list of L<Finance::Alpaca::Struct::Order> objects.

The orders endpoint returns a list of orders for the account, filtered by the
supplied parameters.

The following parameters are accepted:

=over

=item C<status> - Order status to be queried. C<open>, C<closed>, or C<all>. Defaults to C<open>.

=item C<limit> - The maximum number of orders in response. Defaults to C<50> and max is C<500>.

=item C<after> - The response will include only ones submitted after this timestamp (exclusive.)

=item C<until> - The response will include only ones submitted until this timestamp (exclusive.)

=item C<direction> - The chronological order of response based on the submission time. C<asc> or C<desc>. Defaults to C<desc>.

=item C<nested> - Boolean value indicating whether the result will roll up multi-leg orders under the C<legs( )> field of the primary order.

=item C<symbols> - A comma-separated list of symbols to filter by (ex. C<AAPL,TSLA,MSFT>).

=back

=head2 C<order_by_id( ..., [...] )>

    my $order = $camelid->order_by_id('0f43d12c-8f13-4bff-8597-c665b66bace4');

Returns a L<Finance::Alpaca::Struct::Order> object.

You must provide the order's C<id> (UUID). If the order is not found, an empty
list is returned.

You may also provide a boolean value; if true, the result will roll up
multi-leg orders under the C<legs( )> field in primary order.

    my $order = $camelid->order_by_id('0f43d12c-8f13-4bff-8597-c665b66bace4', 1);

=head2 C<order_by_client_id( ... )>

    my $order = $camelid->order_by_client_id('17ff6b86-d330-4ac1-808b-846555b75b6e');

Returns a L<Finance::Alpaca::Struct::Order> object.

You must provide the order's C<client_order_id> (UUID). If the order is not
found, an empty list is returned.

=head2 C<create_order( ... )>

    my $order = $camelid->create_order(
        symbol => 'MSFT',
        qty    => .1,
        side   => 'buy',
        type   => 'market',
        time_in_force => 'day'
    );

If the order is placed successfully, this method returns a
L<Finance::Alpaca::Struct::Order> object. Failures result in hash references
with data from the API.

An order request may be rejected if the account is not authorized for trading,



( run in 0.706 second using v1.01-cache-2.11-cpan-5b529ec07f3 )