Finance-Robinhood

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

0.13 2016-12-25T19:41:16Z

	- Fix adding single instrument to watchlist

0.12 2016-12-02T21:42:01Z

    - Update eg/buy.pl for minor API change

0.11 2016-10-10T16:41:23Z

    - RH's API server stopped updating the /markets/ endpoint on July 30th.
    - ::Market::Hours now exposes extended trading hours

0.10 2016-08-27T16:37:13Z

    - New Finance::Robinhood::Portfolio class for current and historical info
    - New ::Robinhood->portfolios() method to get paginated list of portfolios
    - New ::Robinhood->markets() method to get a paginated list of supported
        markets as Finance::Robinhood::Market objects
    - New ::Robinhood::Market->new(...) constructor to get an object representing a
        specific exchange. Use the ISO 10383 MIC. ...see the docs for more.

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

use Finance::Robinhood::Order;
use Finance::Robinhood::Position;
use Finance::Robinhood::Quote;
use Finance::Robinhood::Watchlist;
use Finance::Robinhood::Portfolio;
#
has token => (is => 'ro', writer => '_set_token');
#
my $base = 'https://api.robinhood.com/';

# Different endpoints we can call for the API
my %endpoints = (
                'accounts'               => 'accounts/',
                'accounts/positions'     => 'accounts/%s/positions/',
                'portfolios'             => 'portfolios/',
                'portfolios/historicals' => 'portfolios/historicals/',
                'ach_deposit_schedules'  => 'ach/deposit_schedules/',
                'ach_iav_auth'           => 'ach/iav/auth/',
                'ach_relationships'      => 'ach/relationships/',
                'ach_transfers'          => 'ach/transfers/',
                'applications'           => 'applications/',
                'dividends'              => 'dividends/',

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

                'user/id'                 => 'user/id/',
                'user/additional_info'    => 'user/additional_info/',
                'user/basic_info'         => 'user/basic_info/',
                'user/employment'         => 'user/employment/',
                'user/investment_profile' => 'user/investment_profile/',
                'user/identity_mismatch'  => 'user/identity_mismatch',
                'watchlists'              => 'watchlists/',
                'watchlists/bulk_add'     => 'watchlists/%s/bulk_add/'
);

sub endpoint {
    $endpoints{$_[0]} ?
        ($DEV > 10 ?
             'http://brokeback.dev.robinhood.com/'
         : 'https://api.robinhood.com/'
        )
        . $endpoints{+shift}
        : ();
}
#
# Send a username and password to Robinhood to get back a token.
#
my ($client, $res);
my %headers = (
         'Accept' => '*/*',
         'Accept-Language' =>
             'en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5',

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

         'User-Agent'              => 'Robinhood/2357 (Android/2.19.0)'
);
sub errors { shift; carp shift; }

sub login {
    my ($self, $username, $password) = @_;

    # Make API Call
    my ($status, $data, $raw)
        = _send_request(undef, 'POST',
                        Finance::Robinhood::endpoint('login'),
                        {username => $username,
                         password => $password
                        }
        );

    # Make sure we have a token.
    if ($status != 200 || !defined($data->{token})) {
        $self->errors(join ' ', @{$data->{non_field_errors}});
        return !1;
    }

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

    # Set the token we just received.
    return $self->_set_token($data->{token});
}

sub logout {
    my ($self) = @_;

    # Make API Call
    my ($status, $rt, $raw)
        = $self->_send_request('POST',
                               Finance::Robinhood::endpoint('logout'));
    return $status == 200 ?

        # The old token is now invalid, so we might as well delete it
        $self->_set_token(())
        : ();
}

sub forgot_password {
    my $self = shift if ref $_[0] && ref $_[0] eq __PACKAGE__;
    my ($email) = @_;

    # Make API Call
    my ($status, $rt, $raw)
        = _send_request(undef, 'POST',
                       Finance::Robinhood::endpoint('password_reset/request'),
                       {email => $email});
    return $status == 200;
}

sub change_password {
    my $self = shift if ref $_[0] && ref $_[0] eq __PACKAGE__;
    my ($user, $password, $token) = @_;

    # Make API Call
    my ($status, $rt, $raw)
        = _send_request(undef, 'POST',
                        Finance::Robinhood::endpoint('password_reset'),
                        {username => $user,
                         password => $password,
                         token    => $token
                        }
        );
    return $status == 200;
}

sub user_info {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET', Finance::Robinhood::endpoint('user'));
    return $status == 200 ?
        map { $_ => $data->{$_} } qw[email id last_name first_name username]
        : ();
}

sub user_id {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                               Finance::Robinhood::endpoint('user/id'));
    return $status == 200 ? $data->{id} : ();
}

sub basic_info {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                             Finance::Robinhood::endpoint('user/basic_info'));
    return $status != 200 ?
        ()
        : ((map { $_ => _2_datetime(delete $data->{$_}) }
                qw[date_of_birth updated_at]
           ),
           map { m[url] ? () : ($_ => $data->{$_}) } keys %$data
        );
}

sub additional_info {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                               Finance::Robinhood::endpoint(
                                                       'user/additional_info')
        );
    return $status != 200 ?
        ()
        : ((map { $_ => _2_datetime(delete $data->{$_}) } qw[updated_at]),
           map { m[user] ? () : ($_ => $data->{$_}) } keys %$data);
}

sub employment_info {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                             Finance::Robinhood::endpoint('user/employment'));
    return $status != 200 ?
        ()
        : ((map { $_ => _2_datetime(delete $data->{$_}) } qw[updated_at]),
           map { m[user] ? () : ($_ => $data->{$_}) } keys %$data);
}

sub investment_profile {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                               Finance::Robinhood::endpoint(
                                                    'user/investment_profile')
        );
    return $status != 200 ?
        ()
        : ((map { $_ => _2_datetime(delete $data->{$_}) } qw[updated_at]),
           map { m[user] ? () : ($_ => $data->{$_}) } keys %$data);
}

sub identity_mismatch {
    my ($self) = @_;
    my ($status, $data, $raw)
        = $self->_send_request('GET',
                               Finance::Robinhood::endpoint(
                                                     'user/identity_mismatch')
        );
    return $status == 200 ? $self->_paginate($data) : ();
}

sub accounts {
    my ($self) = @_;

    # TODO: Deal with next and previous results? Multiple accounts?
    my $return = $self->_send_request('GET',
                                      Finance::Robinhood::endpoint('accounts')
    );
    return $self->_paginate($return, 'Finance::Robinhood::Account');
}
#
# Returns the porfillo summery of an account by url.
#
sub portfolios {
    my ($self) = @_;

    # TODO: Deal with next and previous results? Multiple portfolios?
    my $return =
        $self->_send_request('GET',
                             Finance::Robinhood::endpoint('portfolios'));
    return $self->_paginate($return, 'Finance::Robinhood::Portfolio');
}

sub instrument {

#my $msft      = Finance::Robinhood::instrument('MSFT');
#my $msft      = $rh->instrument('MSFT');
#my ($results) = $rh->instrument({query  => 'FREE'});
#my ($results) = $rh->instrument({cursor => 'cD04NjQ5'});
#my $msft      = $rh->instrument({id     => '50810c35-d215-4866-9758-0ada4ac79ffa'});
    my $self = shift if ref $_[0] && ref $_[0] eq __PACKAGE__;
    my ($type) = @_;
    my $result = _send_request($self, 'GET',
                               Finance::Robinhood::endpoint('instruments')
                                   . (  !defined $type ? ''
                                      : !ref $type     ? '?query=' . $type
                                      : ref $type eq 'HASH'
                                          && defined $type->{cursor}
                                      ? '?cursor=' . $type->{cursor}
                                      : ref $type eq 'HASH'
                                          && defined $type->{query}
                                      ? '?query=' . $type->{query}
                                      : ref $type eq 'HASH'
                                          && defined $type->{id}

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

        };
    }
    return $retval;
}

sub quote {
    my $self = ref $_[0] ? shift : ();    # might be undef but that's okay
                                          #if (scalar @_ > 1 or wantarray) {
    my $return =
        _send_request($self, 'GET',
              Finance::Robinhood::endpoint('quotes') . '?symbols=' . join ',',
              @_);
    return _paginate($self, $return, 'Finance::Robinhood::Quote');

    #}
    #my $quote =
    #    _send_request($self, 'GET',
    #                  Finance::Robinhood::endpoint('quotes') . shift . '/');
    #return $quote ?
    #    Finance::Robinhood::Quote->new($quote)
    #    : ();
}

sub fundamentals {
    my $self = ref $_[0] ? shift : ();    # might be undef but that's okay
                                          #if (scalar @_ > 1 or wantarray) {
    my $return =
        _send_request($self,
                      'GET',
                      Finance::Robinhood::endpoint('fundamentals')
                          . '?symbols='
                          . join ',',
                      @_
        );
    return _paginate($self, $return, 'Finance::Robinhood::Fundamentals');

    #}
    #my $quote =
    #    _send_request($self, 'GET',
    #                  Finance::Robinhood::endpoint('quotes') . shift . '/');
    #return $quote ?
    #    Finance::Robinhood::Quote->new($quote)
    #    : ();
}

sub historicals {
    my $self = ref $_[0] ? shift : ();    # might be undef but that's okay
    my ($symbol, $interval, $span, $bounds) = @_;
    my %fields = (interval => $interval,
                  span     => $span,
                  bounds   => $bounds
    );
    my $fields = join '&', map { $_ . '=' . $fields{$_} }
        grep { defined $fields{$_} } keys %fields;
    my ($status, $data, $raw)
        = _send_request($self,
                        'GET',
                        Finance::Robinhood::endpoint('quotes/historicals')
                            . "$symbol/"
                            . ($fields ? "?$fields" : '')
        );
    return if $status != 200;
    for (@{$data->{historicals}}) {
        $_->{begins_at} = _2_datetime($_->{begins_at});
    }
    return $data->{historicals};
}

sub locate_order {
    my ($self, $order_id) = @_;
    my $result = $self->_send_request('GET',
                    Finance::Robinhood::endpoint('orders') . $order_id . '/');
    return $result ?
        Finance::Robinhood::Order->new(rh => $self, %$result)
        : ();
}

sub list_orders {
    my ($self, $type) = @_;
    my $result = $self->_send_request(
            'GET',
            Finance::Robinhood::endpoint('orders')
                . (
                ref $type
                    && ref $type eq 'HASH'
                    && defined $type->{cursor} ? '?cursor=' . $type->{cursor}
                : ref $type && ref $type eq 'HASH' && defined $type->{'since'}
                ? '?updated_at[gte]=' . $type->{'since'}
                : ref $type
                    && ref $type eq 'HASH'
                    && defined $type->{'instrument'}
                    && 'Finance::Robinhood::Instrument' eq

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

                : ''
                )
    );
    $result // return !1;
    return () if !$result;
    return $self->_paginate($result, 'Finance::Robinhood::Order');
}

# Methods under construction
sub cards {
    return shift->_send_request('GET', Finance::Robinhood::endpoint('cards'));
}

sub dividends {
    return
        shift->_send_request('GET',
                             Finance::Robinhood::endpoint('dividends'));
}

sub notifications {
    return
        shift->_send_request('GET',
                             Finance::Robinhood::endpoint('notifications'));
}

sub notifications_devices {
    return
        shift->_send_request('GET',
                             Finance::Robinhood::endpoint(
                                                      'notifications/devices')
        );
}

sub create_watchlist {
    my ($self, $name) = @_;
    my ($status, $result)
        = $self->_send_request('POST',
                               Finance::Robinhood::endpoint('watchlists'),
                               {name => $name});
    return $status == 201
        ?
        Finance::Robinhood::Watchlist->new(rh => $self, %$result)
        : ();
}

sub delete_watchlist {
    my ($self, $watchlist) = @_;
    my ($status, $result, $response)
        = $self->_send_request('DELETE',
                               Finance::Robinhood::endpoint('watchlists')
                                   . (ref $watchlist ?
                                          $watchlist->name()
                                      : $watchlist
                                   )
                                   . '/'
        );
    return $status == 204;
}

sub watchlists {
    my ($self, $cursor) = @_;
    my $result = $self->_send_request('GET',
                                      Finance::Robinhood::endpoint(
                                                                 'watchlists')
                                          . (
                                            ref $cursor
                                                && ref $cursor eq 'HASH'
                                                && defined $cursor->{cursor}
                                            ?
                                                '?cursor=' . $cursor->{cursor}
                                            : ''
                                          )
    );
    $result // return !1;
    return () if !$result;
    return $self->_paginate($result, 'Finance::Robinhood::Watchlist');
}

sub watchlist {
    my ($self, $name) = @_;
    my ($status, $result)
        = $self->_send_request('GET',
                       Finance::Robinhood::endpoint('watchlists') . "$name/");
    return $status == 200 ?
        Finance::Robinhood::Watchlist->new(name => $name,
                                           rh   => $self,
                                           %$result
        )
        : ();
}

sub markets {
    my $self = ref $_[0] ? shift : ();    # might be undef but that's okay
    my ($symbol, $interval, $span) = @_;
    my $result = _send_request(undef, 'GET',
                               Finance::Robinhood::endpoint('markets'));
    return _paginate($self, $result, 'Finance::Robinhood::Market');
}

# TESTING!
# @GET("/documents/{id}/download/?redirect=False")
#    Observable<DocumentDownloadResponse> getDocumentDownloadUrl(@Path("id") String str);
sub documents_download {
    my ($s, $id, $redirect) = @_;
    warn Finance::Robinhood::endpoint('documents/download');
    my $result =
        _send_request($s, 'GET',
                   sprintf Finance::Robinhood::endpoint('documents/download'),
                   $id, $redirect ? 'True' : 'False');

    #return _paginate( $self, $result, 'Finance::Robinhood::Market' );
    $result;
}

# ---------------- Private Helper Functions --------------- //
# Send request to API.
#
sub _paginate {    # Paginates results

lib/Finance/Robinhood/Account.pm  view on Meta::CPAN

           required => 1,
           coerce   => \&Finance::Robinhood::_2_datetime
) for (qw[updated_at]);
has $_ => (is => 'bare', required => 1, accessor => "_get_$_", weak_ref => 1)
    for (qw[rh]);

sub positions {
    my ($self, $type) = @_;
    my ($status, $result, $raw) = $self->_get_rh()->_send_request(
        'GET',
        sprintf(Finance::Robinhood::endpoint('accounts/positions'),
                $self->account_number()
            )
            . sub {
            my $opt = shift;
            return '' if !ref $opt || ref $type ne 'HASH';
            return '?cursor=' . $opt->{cursor} if defined $opt->{cursor};
            return '?nonzero=' . ($opt->{nonzero} ? 'true' : 'false')
                if defined $opt->{nonzero};
            return '';
        }

lib/Finance/Robinhood/Account.pm  view on Meta::CPAN

    );
    return
        Finance::Robinhood::_paginate($self->_get_rh(), $result,
                                      'Finance::Robinhood::Position');
}

sub portfolio {
    my ($self) = @_;
    my ($status, $result, $raw)
        = $self->_get_rh()->_send_request('GET',
                                    Finance::Robinhood::endpoint('portfolios')
                                        . $self->account_number()
                                        . '/');
    return $result;
}

sub historicals {
    my ($self, $interval, $span) = @_;
    my ($status, $result, $raw)
        = $self->_get_rh()->_send_request('GET',
                        Finance::Robinhood::endpoint('portfolios/historicals')
                            . $self->account_number()
                            . "/?interval=$interval&span=$span");
    return () if $status != 200;
    for (@{$result->{equity_historicals}}) {
        $_->{begins_at} = Finance::Robinhood::_2_datetime($_->{begins_at});
    }
    return $result;
}
1;

lib/Finance/Robinhood/Fundamentals.pm  view on Meta::CPAN

has $_ => (is => 'lazy', reader => "_get_$_", clearer => 1) for (qw[raw]);

sub _build_raw {
    my $s = shift;
    my $url;
    if ($s->has_url) {
        $url = $s->_get_url;
    }

    #elsif ($s->has_id) {
    #    $url = Finance::Robinhood::endpoint('instruments') . $s->id . '/';
    #}
    else {
        return {}    # We done messed up!
    }
    my ($status, $result, $raw)
        = Finance::Robinhood::_send_request(undef, 'GET', $url);
    return $result;
}

sub refresh {

lib/Finance/Robinhood/Instrument.pm  view on Meta::CPAN

) for (qw[quote market splits]);
has $_ => (is => 'lazy', reader => "_get_$_") for (qw[raw]);

sub _build_raw {
    my $s = shift;
    my $url;
    if ($s->has_url) {
        $url = $s->url;
    }
    elsif ($s->has_id) {
        $url = Finance::Robinhood::endpoint('instruments') . $s->id . '/';
    }
    else {
        return {}    # We done messed up!
    }
    my ($status, $result, $raw)
        = Finance::Robinhood::_send_request(undef, 'GET', $url);
    return $result;
}

sub _build_quote {

lib/Finance/Robinhood/Market.pm  view on Meta::CPAN

    shift->_get_raw()->{url};
}

sub _build_raw {
    my $s = shift;
    my $url;
    if ($s->has_url) {
        $url = $s->_get_url;
    }
    elsif ($s->has_mic) {
        $url = Finance::Robinhood::endpoint('markets') . $s->mic . '/';
    }
    else {
        return {}    # We done messed up!
    }
    my ($status, $result, $raw)
        = Finance::Robinhood::_send_request(undef, 'GET', $url);
    return $result;
}

sub todays_hours {

lib/Finance/Robinhood/Order.pm  view on Meta::CPAN

has $_ => (is => 'bare', required => 1, accessor => "_get_$_")
    for (qw[account instrument]);
around BUILDARGS => sub {
    my ($orig, $class, @args) = @_;

    # If this is a new order, create it with the API first
    if (!defined {@args}->{url}) {
        my ($status, $data, $raw)
            = {@args}->{account}->_get_rh()->_send_request(
            'POST',
            Finance::Robinhood::endpoint('orders'),
            {account    => {@args}->{account}->_get_url(),
             instrument => {@args}->{instrument}->url(),
             symbol     => {@args}->{instrument}->symbol(),
             price      => {@args}->{price}
                 // {@args}->{instrument}->last_extended_hours_trade_price()
                 // {@args}->{instrument}->quote->bid_price(),
             (map {
                  {@args}
                  ->{$_} ? ($_ => ({@args}->{$_} ? 'true' : 'false')) : ()
              } qw[override_dtbp_checks extended_hours override_day_trade_checks]

lib/Finance/Robinhood/Portfolio.pm  view on Meta::CPAN

#
sub BUILDARGS {
    my $class = shift;
    return @_ > 1 ?
        {@_}
        : {
          (rh => $_[0][0],
           %{  +Finance::Robinhood::_send_request(
                   $_[0][0],
                   'GET',
                   Finance::Robinhood::endpoint('portfolios') . $_[0][1] . '/'
               )
           }
          )
        };

    # if the scrape failed (bad id, etc.) let Moo error out :)
}
has $_ => (is => 'ro', required => 1)
    for (qw[adjusted_equity_previous_close equity equity_previous_close
         excess_maintenance excess_maintenance_with_uncleared_deposits

lib/Finance/Robinhood/Portfolio.pm  view on Meta::CPAN

    return $result
        ?
        Finance::Robinhood::Account->new(rh => $self->_get_rh, %$result)
        : ();
}

sub historicals {
    my ($self, $interval, $span) = @_;
    return
        scalar $self->_get_rh()->_send_request('GET',
                        Finance::Robinhood::endpoint('portfolios/historicals')
                            . $self->id
                            . "/?interval=$interval&span=$span");
}

sub id {
    my $_re = Finance::Robinhood::endpoint('portfolios') . '(.+)/';
    shift->url =~ m[$_re]o;
    $1;
}

sub refresh {
    return $_[0]
        = Finance::Robinhood::Portfolio->new([$_[0]->_get_rh, $_[0]->id]);
}
1;

lib/Finance/Robinhood/Quote.pm  view on Meta::CPAN

has $_ => (is => 'lazy', reader => "_get_$_") for (qw[raw]);

sub _build_raw {
    my $s = shift;
    my $url;
    if ($s->has_url) {
        $url = $s->_get_url;
    }

    #elsif ($s->has_id) {
    #    $url = Finance::Robinhood::endpoint('instruments') . $s->id . '/';
    #}
    else {
        return {}    # We done messed up!
    }
    my ($status, $result, $raw)
        = Finance::Robinhood::_send_request(undef, 'GET', $url);
    return $result;
}
1;

lib/Finance/Robinhood/Watchlist.pm  view on Meta::CPAN

    isa      => sub {
        die "$_[0] is not an Finance::Robinhood object!"
            unless ref $_[0] eq 'Finance::Robinhood';
    },
    weak_ref => 1
) for (qw[rh]);

sub instruments {
    my $self = shift;
    my $res  = $self->_get_rh()->_send_request('GET',
                    Finance::Robinhood::endpoint('watchlists') . $self->name);
    return
        $self->_get_rh()->_paginate({results => [
                                             map { {url => $_->{instrument}} }
                                                 @{delete $res->{results}}
                                     ],
                                     %$res
                                    },
                                    'Finance::Robinhood::Instrument'
        );
}

lib/Finance/Robinhood/Watchlist.pm  view on Meta::CPAN

    my $self = shift;
    my (@symbols, $retval);

    # Divide list of symbols into groups of 32 max
    @{$symbols[$_]} = grep defined, @_[$_ * 32 .. ($_ + 1) * 32 - 1]
        for 0 .. $#_ / 32;
    for my $group (@symbols) {
        my ($status, $result)
            = $self->_get_rh()->_send_request(
                  'POST',
                  sprintf(Finance::Robinhood::endpoint('watchlists/bulk_add'),
                          $self->name
                  ),
                  {symbols => join ',', @$group}
            );
        push @$retval, @$result if $status == 201;
    }
    return $retval ?
        map {
        my ($status, $instrument, $raw)
            = $self->_get_rh()->_send_request('GET', $_->{instrument});
        Finance::Robinhood::Instrument->new($instrument)
        } @{$retval}
        : ();
}

sub add_instrument {
    my ($self, $instrument) = @_;
    my $ret =
        $self->_get_rh()->_send_request(
             'POST',
             Finance::Robinhood::endpoint('watchlists') . $self->name() . '/',
             {instrument => $instrument->_get_url()}
        );
    return $ret;
}

sub delete_instrument {
    my ($self, $instrument) = @_;
    my ($status, $ret)
        = $self->_get_rh()->_send_request('DELETE',
                                    Finance::Robinhood::endpoint('watchlists')
                                        . $self->name() . '/'
                                        . $instrument->id()
                                        . '/');
    return $status == 204;
}
1;

=encoding utf-8

=head1 NAME



( run in 0.480 second using v1.01-cache-2.11-cpan-beeb90c9504 )