view release on metacpan or search on metacpan
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