Finance-Robinhood/lib/Finance/Robinhood.pm
package Finance::Robinhood;
=encoding utf-8
=for stopwords watchlist watchlists untradable urls forex
=head1 NAME
Finance::Robinhood - Trade Stocks, ETFs, Options, and Cryptocurrency without
Commission
=head1 SYNOPSIS
use Finance::Robinhood;
my $rh = Finance::Robinhood->new();
=cut
our $VERSION = '0.92_003';
#
use Mojo::Base-base, -signatures;
use Mojo::UserAgent;
use Mojo::URL;
#
use Finance::Robinhood::Error;
use Finance::Robinhood::Utilities qw[gen_uuid];
use Finance::Robinhood::Utilities::Iterator;
use Finance::Robinhood::OAuth2::Token;
=head1 METHODS
Finance::Robinhood wraps several APIs. There are parts of this package that
will not apply because your account does not have access to certain features.
=head2 C<new( )>
Robinhood requires an authorization token for most API calls. To get this
token, you must log in with your username and password. But we'll get into that
later. For now, let's create a client object...
# You can look up some basic instrument data with this
my $rh = Finance::Robinhood->new();
A new Finance::Robinhood object is created without credentials. Before you can
buy or sell or do almost anything else, you must L<log in|/"login( ... )">.
=head3 C<token =E<gt> ...>
If you have previously authorized this package to access your account, passing
the OAuth2 tokens here will prevent you from having to C<login( ... )> with
your user data.
These tokens should be kept private.
=head3 C<device_token =E<gt> ...>
If you have previously authorized this package to access your account, passing
the assigned device ID here will prevent you from having to authorize it again
upon C<login( ... )>.
Like authorization tokens, this UUID should be kept private.
=cut
has _ua => sub {
my $x = Mojo::UserAgent->new;
$x->transactor->name(sprintf 'Perl/%s (%s) %s/%s',
($^V =~ m[([\.\d]+)]),
$^O, __PACKAGE__, $VERSION);
$x;
};
BEGIN { # Hide giant UA from Data::Dump
Data::Dump::Filtered::add_dump_filter(
sub {
my ($ctx, $object) = @_;
$ctx->is_blessed &&
$ctx->class eq 'Mojo::UserAgent'
? {'dump' => 'Mojo::UserAgent object {' .
$object->transactor->name . '}'
}
: ();
}
) if $Data::Dump::VERSION && require Data::Dump::Filtered;
}
has 'oauth2_token';
has 'device_token' => sub { gen_uuid() };
sub _test_new {
ok(t::Utility::rh_instance(1));
}
sub _get ($s, $url, %data) {
$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{$data{$_}} : $data{$_}
for keys %data;
$url = Mojo::URL->new($url);
$url->query(\%data);
#warn 'GET ' . $url;
#warn ' Auth: ' . (
# ( $s->oauth2_token && $url =~ m[^https://[a-z]+\.robinhood\.com/.+$] ) ? $s->oauth2_token->token_type :
# 'none' );
my $retval = $s->_ua->get(
$url => {
($s->oauth2_token &&
$url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
)
? ('Authorization' => ucfirst join ' ',
$s->oauth2_token->token_type, $s->oauth2_token->access_token
)
: ()
}
);
#use Data::Dump;
#warn ' Result: ' . $retval->res->code;
#die if $retval->res->code == 401;
#use Data::Dump;
#ddx $retval->res->headers;
#ddx $retval;
#warn $retval->res->code;
#ddx $retval->res;
#warn $retval->res->body;
return $s->_get($url, %data)
if $retval->res->code == 401 && $s->_refresh_login_token;
$retval->result;
}
sub _test_get {
my $rh = t::Utility::rh_instance(0);
my $res = $rh->_get('https://jsonplaceholder.typicode.com/todos/1');
isa_ok($res, 'Mojo::Message::Response');
is($res->json->{title}, 'delectus aut autem', '_post(...) works!');
#
$res = $rh->_get('https://httpstat.us/500');
isa_ok($res, 'Mojo::Message::Response');
ok(!$res->is_success);
}
sub _options ($s, $url, %data) {
my $retval = $s->_ua->options(
Mojo::URL->new($url) => {
($s->oauth2_token &&
$url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
)
? ('Authorization' => ucfirst join ' ',
$s->oauth2_token->token_type, $s->oauth2_token->access_token
)
: ()
} => json => \%data
);
return $s->_options($url, %data)
if $retval->res->code == 401 && $s->_refresh_login_token;
$retval->result;
}
sub _test_options {
my $rh = t::Utility::rh_instance(0);
my $res = $rh->_options('https://jsonplaceholder.typicode.com/');
isa_ok($res, 'Mojo::Message::Response');
is($res->json, ());
}
sub _post ($s, $url, %data) {
#warn 'POST ' . $url;
#use Data::Dump;
#ddx \%data;
#$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
#warn ' Auth: ' . (($s->oauth2_token && $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]) ? $s->oauth2_token->token_type : 'none');
my $retval = $s->_ua->post(
Mojo::URL->new($url) => {
($s->oauth2_token &&
$url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
) &&
!delete $data{'no_auth_token'}
? ('Authorization' => ucfirst join ' ',
$s->oauth2_token->token_type, $s->oauth2_token->access_token
)
: (),
($data{challenge_id}
? ('X-Robinhood-Challenge-Response-ID' =>
delete $data{challenge_id})
: ()
)
} => json => \%data
);
#use Data::Dump;
#warn ' Result: ' . $retval->res->code;
#die if $retval->res->code == 401;
#use Data::Dump;
#ddx $retval->res->headers;
#ddx $retval;
#warn $retval->res->code;
#ddx $retval->res;
#warn $retval->res->body;
return $s->_post($url, %data) # Retry with new auth info
if $retval->res->code == 401 && $s->_refresh_login_token;
$retval->res;
}
sub _test_post {
my $rh = t::Utility::rh_instance(0);
my $res = $rh->_post('https://jsonplaceholder.typicode.com/posts/',
title => 'Whoa',
body => 'This is a test',
userId => 13
);
isa_ok($res, 'Mojo::Message::Response');
is($res->json,
{body => 'This is a test', title => 'Whoa', userId => 13, id => 101});
}
sub _patch ($s, $url, %data) {
#$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
my $retval = $s->_ua->patch(
Mojo::URL->new($url) => {
($s->oauth2_token &&
$url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
) &&
!delete $data{'no_auth_token'}
? ('Authorization' => ucfirst join ' ',
$s->oauth2_token->token_type, $s->oauth2_token->access_token
)
: ()
} => json => \%data
);
return $s->_post($url, %data)
if $retval->res->code == 401 && $s->_refresh_login_token;
$retval->result;
}
sub _test_patch {
my $rh = t::Utility::rh_instance(0);
my $res = $rh->_patch('https://jsonplaceholder.typicode.com/posts/9/',
title => 'Updated');
isa_ok($res, 'Mojo::Message::Response');
is($res->json->{title}, 'Updated');
}
sub _delete ($s, $url, %data) {
#$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
my $retval = $s->_ua->delete(
Mojo::URL->new($url) => {
($s->oauth2_token &&
$url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
) &&
!delete $data{'no_auth_token'}
? ('Authorization' => ucfirst join ' ',
$s->oauth2_token->token_type, $s->oauth2_token->access_token
)
: ()
} => json => \%data
);
return $s->_delete($url, %data)
if $retval->res->code == 401 && $s->_refresh_login_token;
$retval->result;
}
sub _test_delete {
my $rh = t::Utility::rh_instance(0);
my $res = $rh->_patch('https://jsonplaceholder.typicode.com/posts/1/');
isa_ok($res, 'Mojo::Message::Response');
ok($res->is_success, 'Deleted'); # Lies
}
=head2 C<login( ... )>
my $rh = Finance::Robinhood->new()->login($user, $pass);
A new Finance::Robinhood object is created without credentials. Before you can
buy or sell or do almost anything else, you must L<log in|/"login( ... )">.
=head3 C<mfa_callback =E<gt> ...>
my $rh = Finance::Robinhood->new()->login($user, $pass, mfa_callback => sub {
# Do something like pop open an inputbox in TK, read from shell or whatever
} );
If you have MFA enabled, you may (or must) also pass a callback. When the code
is called, a ref will be passed that will contain C<mfa_required> (a boolean
value) and C<mfa_type> which might be C<app>, C<sms>, etc. Your return value
must be the MFA code.
=head3 C<mfa_code =E<gt> ...>
my $rh = Finance::Robinhood->new()->login($user, $pass, mfa_code => 980385);
If you already know the MFA code (for example if you have MFA enabled through
an app), you can pass that code directly and log in.
=head3 C<challenge_callback =E<gt> ...>
my $rh = Finance::Robinhood->new()->login($user, $pass, challenge_callback => sub {
my ($challenge) = @_;
# Do something like pop open an inputbox in TK, read from shell or whatever
$challenge->respond( ... );
$challenge;
} );
When logging in with a new client, you are required to authorize it to access
your account.
This callback should should expect a Finance::Robinhood::Error::Challenge
object and must return the object after validation.
=head2 C<device_token( [...] )>
my $token = $rh->device_token;
# Store it
To prevent your client from having to be reauthorized to access your account
every time it is run, call this method which returns the device token which
should be passed to C<new( ... )>.
# Reload token from storage
my $device = ...;
$rh->device_token($device);
To prevent your client from having to reauthorize every time it is run, call
this to reload the same ID.
=head2 C<oauth2_token( [...] )>
my $token $rh->oauth2_token;
# Store it
To prevent your client from having to log in every time it is run, call this
method which returns the authorization tokens which should be passed to C<new(
... )>.
This method returns a Finance::Robinhood::OAuth2::Token object.
# Load token object from storage
my $oauth = ...;
$rh->oauth2_token($token);
Reload OAuth2 tokens. You can skip logging in with your username and password
if this is successful.
This method expects a Finance::Robinhood::OAuth2::Token object.
=cut
sub login ($s, $u, $p, %opt) {
# OAUTH2
my $res = $s->_post(
'https://api.robinhood.com/oauth2/token/',
no_auth_token => 1, # NO AUTH INFO SENT!
challenge_type => 'email',
($opt{challenge_id} ? (challenge_id => $opt{challenge_id}) : ()),
device_token => $s->device_token,
expires_in => 86400,
scope => 'internal',
username => $u,
password => $p,
($opt{mfa_code} ? (mfa_code => $opt{mfa_code}) : ()),
grant_type => ($opt{grant_type} // 'password'),
client_id => $opt{client_id} // sub {
my (@k, $c) = split //, shift;
map { # cheap and easy
unshift @k, pop @k;
$c .= chr(ord ^ ord $k[0]);
} split //,
"\aW];&Y55\35I[\a,6&>[5\34\36\f\2]]\$\x179L\\\x0B4<;,\"*&\5);";
$c;
}
->(__PACKAGE__)
);
if ($res->is_success) {
if ($res->json->{mfa_required}) {
return $opt{mfa_callback}
? Finance::Robinhood::Error->new(
description => 'You must pass an mfa_callback.')
: $s->login($u, $p, %opt,
mfa_code => $opt{mfa_callback}->($res->json));
}
else {
require Finance::Robinhood::OAuth2::Token;
$s->oauth2_token(
Finance::Robinhood::OAuth2::Token->new($res->json));
}
}
elsif ($res->json->{challenge}) { # 400
require Finance::Robinhood::Error::Challenge;
return Finance::Robinhood::Error->new(
description => 'You must pass a challenge_callback.')
if !$opt{challenge_callback};
my $challenge =
$opt{challenge_callback}->(
Finance::Robinhood::Error::Challenge->new(
_rh => $s,
%{$res->json->{challenge}}
)
); # Call it
return $challenge
? $s->login($u, $p, %opt, challenge_id => $challenge->id)
: $challenge;
}
else {
return Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
$s;
}
sub _test_login {
my $rh = t::Utility::rh_instance(1);
isa_ok($rh->oauth2_token, 'Finance::Robinhood::OAuth2::Token');
}
# Cannot test this without using the same token for 24hrs and letting it expire
sub _refresh_login_token ($s, %opt)
{ # TODO: Store %opt from login and reuse it here
$s->oauth2_token // return; # OAUTH2
my $res = $s->_post(
'https://api.robinhood.com/oauth2/token/',
no_auth_token => 1, # NO AUTH INFO SENT!
scope => 'internal',
refresh_token => $s->oauth2_token->refresh_token,
grant_type => ($opt{grant_type} // 'password'),
client_id => $opt{client_id} // sub {
my (@k, $c) = split //, shift;
map { # cheap and easy
unshift @k, pop @k;
$c .= chr(ord ^ ord $k[0]);
} split //,
"\aW];&Y55\35I[\a,6&>[5\34\36\f\2]]\$\x179L\\\x0B4<;,\"*&\5);";
$c;
}
->(__PACKAGE__),
);
if ($res->is_success) {
require Finance::Robinhood::OAuth2::Token;
$s->oauth2_token(Finance::Robinhood::OAuth2::Token->new($res->json));
}
else {
return Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
$s;
}
=head2 C<search( ... )>
my $results = $rh->search('microsoft');
Returns a set of search results as a Finance::Robinhood::Search object.
You do not need to be logged in for this to work.
=cut
sub search ($s, $keyword) {
my $res = $s->_get('https://midlands.robinhood.com/search/',
query => $keyword);
require Finance::Robinhood::Search;
$res->is_success
? Finance::Robinhood::Search->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_search {
my $rh = t::Utility::rh_instance(1);
isa_ok($rh->search('tesla'), 'Finance::Robinhood::Search');
}
=head2 C<news( ... )>
my $news = $rh->news('MSFT');
my $news = $rh->news('1072fc76-1862-41ab-82c2-485837590762'); # Forex - USD
An iterator containing Finance::Robinhood::News objects is returned.
=cut
sub news ($s, $symbol_or_id) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://midlands.robinhood.com/news/')->query(
{ ( $symbol_or_id
=~ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
? 'currency_id'
: 'symbol'
) => $symbol_or_id
}
),
_class => 'Finance::Robinhood::News'
);
}
sub _test_news {
my $rh = t::Utility::rh_instance();
my $msft = $rh->news('MSFT');
isa_ok($msft, 'Finance::Robinhood::Utilities::Iterator');
$msft->has_next
? isa_ok($msft->next, 'Finance::Robinhood::News')
: pass('Fake it... Might not be any news on the weekend');
my $btc = $rh->news('d674efea-e623-4396-9026-39574b92b093');
isa_ok($btc, 'Finance::Robinhood::Utilities::Iterator');
$btc->has_next
? isa_ok($btc->next, 'Finance::Robinhood::News')
: pass('Fake it... Might not be any news on the weekend');
}
=head2 C<feed( )>
my $feed = $rh->feed();
An iterator containing Finance::Robinhood::News objects is returned. This list
will be filled with news related to instruments in your watchlist and
portfolio.
You need to be logged in for this to work.
=cut
sub feed ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://midlands.robinhood.com/feed/',
_class => 'Finance::Robinhood::News'
);
}
sub _test_feed {
my $feed = t::Utility::rh_instance(1)->feed;
isa_ok($feed, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($feed->current, 'Finance::Robinhood::News');
}
=head2 C<notifications( )>
my $cards = $rh->notifications();
An iterator containing Finance::Robinhood::Notification objects is returned.
You need to be logged in for this to work.
=cut
sub notifications ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://midlands.robinhood.com/notifications/stack/',
_class => 'Finance::Robinhood::Notification'
);
}
sub _test_notifications {
my $cards = t::Utility::rh_instance(1)->notifications;
isa_ok($cards, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($cards->current, 'Finance::Robinhood::Notification');
}
=head2 C<notification_by_id( ... )>
my $card = $rh->notification_by_id($id);
Returns a Finance::Robinhood::Notification object. You need to be logged in for
this to work.
=cut
sub notification_by_id ($s, $id) {
my $res = $s->_get(
'https://midlands.robinhood.com/notifications/stack/' . $id . '/');
require Finance::Robinhood::Notification if $res->is_success;
return $res->is_success
? Finance::Robinhood::Notification->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_notification_by_id {
my $rh = t::Utility::rh_instance(1);
my $card = $rh->notification_by_id($rh->notifications->current->id);
isa_ok($card, 'Finance::Robinhood::Notification');
}
=head1 EQUITY METHODS
=head2 C<equity_instruments( )>
my $instruments = $rh->equity_instruments();
Returns an iterator containing equity instruments.
You may restrict, search, or modify the list of instruments returned with the
following optional arguments:
=over
=item C<symbol> - Ticker symbol
my $msft = $rh->equity_instruments(symbol => 'MSFT')->next;
By the way, C<instrument_by_symbol( )> exists as sugar. It returns the
instrument itself rather than an iterator object with a single element.
=item C<query> - Keyword search
my @solar = $rh->equity_instruments(query => 'solar')->all;
=item C<ids> - List of instrument ids
my ( $msft, $tsla )
= $rh->equity_instruments(
ids => [ '50810c35-d215-4866-9758-0ada4ac79ffa', 'e39ed23a-7bd1-4587-b060-71988d9ef483' ] )
->all;
If you happen to know/store instrument ids, quickly get full instrument objects
this way.
=back
=cut
sub equity_instruments ($s, %filter) {
$filter{ids} = join ',', @{$filter{ids}}
if $filter{ids}; # Has to be done manually
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://api.robinhood.com/instruments/')
->query(\%filter),
_class => 'Finance::Robinhood::Equity::Instrument'
);
}
sub _test_equity_instruments {
my $rh = t::Utility::rh_instance(0);
my $instruments = $rh->equity_instruments;
isa_ok($instruments, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($instruments->next, 'Finance::Robinhood::Equity::Instrument');
#
{
my $msft = $rh->equity_instruments(symbol => 'MSFT')->current;
isa_ok($msft, 'Finance::Robinhood::Equity::Instrument');
is($msft->symbol, 'MSFT',
'equity_instruments(symbol => "MSFT") returned Microsoft');
}
#
{
my $tsla = $rh->equity_instruments(query => 'tesla')->current;
isa_ok($tsla, 'Finance::Robinhood::Equity::Instrument');
is($tsla->symbol, 'TSLA',
'equity_instruments(query => "tesla") returned Tesla');
}
{
my ($msft, $tsla)
= $rh->equity_instruments(
ids => [
'50810c35-d215-4866-9758-0ada4ac79ffa',
'e39ed23a-7bd1-4587-b060-71988d9ef483'
]
)->all;
isa_ok($msft, 'Finance::Robinhood::Equity::Instrument');
is($msft->symbol, 'MSFT',
'equity_instruments( ids => ... ) returned Microsoft');
isa_ok($tsla, 'Finance::Robinhood::Equity::Instrument');
is($tsla->symbol, 'TSLA',
'equity_instruments( ids => ... ) also returned Tesla');
}
}
=head2 C<equity_instrument_by_symbol( ... )>
my $instrument = $rh->equity_instrument_by_symbol('MSFT');
Searches for an equity instrument by ticker symbol and returns a
Finance::Robinhood::Equity::Instrument.
=cut
sub equity_instrument_by_symbol ($s, $symbol) {
$s->equity_instruments(symbol => $symbol)->current;
}
sub _test_equity_instrument_by_symbol {
my $rh = t::Utility::rh_instance(0);
my $instrument = $rh->equity_instrument_by_symbol('MSFT');
isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
}
=head2 C<equity_instrument_by_id( ... )>
my $instrument = $rh->equity_instrument_by_id('50810c35-d215-4866-9758-0ada4ac79ffa');
Searches for a single of equity instrument by its instrument id and returns a
Finance::Robinhood::Equity::Instrument object.
=cut
sub equity_instrument_by_id ($s, $id) {
$s->equity_instruments(ids => [$id])->next();
}
sub _test_equity_instrument_by_id {
my $rh = t::Utility::rh_instance(0);
my $instrument = $rh->equity_instrument_by_id(
'50810c35-d215-4866-9758-0ada4ac79ffa');
isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
is($instrument->symbol, 'MSFT',
'equity_instruments( ids => ... ) returned Microsoft');
}
=head2 C<equity_instruments_by_id( ... )>
my $instrument = $rh->equity_instruments_by_id('50810c35-d215-4866-9758-0ada4ac79ffa');
Searches for a list of equity instruments by their instrument ids and returns a
list of Finance::Robinhood::Equity::Instrument objects.
=cut
sub equity_instruments_by_id ($s, @ids) {
# Split ids into groups of 75 to keep URL length down
my @retval;
push @retval, $s->equity_instruments(ids => [splice @ids, 0, 75])->all()
while @ids;
@retval;
}
sub _test_equity_instruments_by_id {
my $rh = t::Utility::rh_instance(0);
my ($instrument)
= $rh->equity_instruments_by_id(
'50810c35-d215-4866-9758-0ada4ac79ffa');
isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
is($instrument->symbol, 'MSFT',
'equity_instruments( ids => ... ) returned Microsoft');
}
=head2 C<equity_orders( [...] )>
my $orders = $rh->equity_orders();
An iterator containing Finance::Robinhood::Equity::Order objects is returned.
You need to be logged in for this to work.
my $orders = $rh->equity_orders(instrument => $msft);
If you would only like orders after a certain date, you can do that!
my $orders = $rh->equity_orders(after => Time::Moment->now->minus_days(7));
# Also accepts ISO 8601
If you would only like orders before a certain date, you can do that!
my $orders = $rh->equity_orders(before => Time::Moment->now->minus_years(2));
# Also accepts ISO 8601
=cut
sub equity_orders ($s, %opts) {
#- `updated_at[gte]` - greater than or equal to a date; timestamp or ISO 8601
#- `updated_at[lte]` - less than or equal to a date; timestamp or ISO 8601
#- `instrument` - equity instrument URL
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://api.robinhood.com/orders/')->query(
{$opts{instrument}
? (instrument => $opts{instrument}->url)
: (),
$opts{before} ? ('updated_at[lte]' => +$opts{before}) : (),
$opts{after} ? ('updated_at[gte]' => +$opts{after}) : ()
}
),
_class => 'Finance::Robinhood::Equity::Order'
);
}
sub _test_equity_orders {
my $rh = t::Utility::rh_instance(1);
my $orders = $rh->equity_orders;
isa_ok($orders, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($orders->next, 'Finance::Robinhood::Equity::Order');
}
=head2 C<equity_order_by_id( ... )>
my $order = $rh->equity_order_by_id($id);
Returns a Finance::Robinhood::Equity::Order object. You need to be logged in
for this to work.
=cut
sub equity_order_by_id ($s, $id) {
my $res = $s->_get('https://api.robinhood.com/orders/' . $id . '/');
require Finance::Robinhood::Equity::Order if $res->is_success;
return $res->is_success
? Finance::Robinhood::Equity::Order->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_equity_order_by_id {
my $rh = t::Utility::rh_instance(1);
my $order = $rh->equity_order_by_id($rh->equity_orders->current->id);
isa_ok($order, 'Finance::Robinhood::Equity::Order');
}
=head2 C<equity_accounts( )>
my $accounts = $rh->equity_accounts();
An iterator containing Finance::Robinhood::Equity::Account objects is returned.
You need to be logged in for this to work.
=cut
sub equity_accounts ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/accounts/',
_class => 'Finance::Robinhood::Equity::Account'
);
}
sub _test_equity_accounts {
my $rh = t::Utility::rh_instance(1);
my $accounts = $rh->equity_accounts;
isa_ok($accounts, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($accounts->current, 'Finance::Robinhood::Equity::Account');
}
=head2 C<equity_account_by_account_number( ... )>
my $account = $rh->equity_account_by_account_number($id);
Returns a Finance::Robinhood::Equity::Account object. You need to be logged in
for this to work.
=cut
sub equity_account_by_account_number ($s, $id) {
my $res = $s->_get('https://api.robinhood.com/accounts/' . $id . '/');
require Finance::Robinhood::Equity::Account if $res->is_success;
return $res->is_success
? Finance::Robinhood::Equity::Account->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_equity_account_by_account_number {
my $rh = t::Utility::rh_instance(1);
my $acct = $rh->equity_account_by_account_number(
$rh->equity_accounts->current->account_number);
isa_ok($acct, 'Finance::Robinhood::Equity::Account');
}
=head2 C<equity_portfolios( )>
my $equity_portfolios = $rh->equity_portfolios();
An iterator containing Finance::Robinhood::Equity::Account::Portfolio objects
is returned. You need to be logged in for this to work.
=cut
sub equity_portfolios ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/portfolios/',
_class => 'Finance::Robinhood::Equity::Account::Portfolio'
);
}
sub _test_equity_portfolios {
my $rh = t::Utility::rh_instance(1);
my $equity_portfolios = $rh->equity_portfolios;
isa_ok($equity_portfolios, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($equity_portfolios->current,
'Finance::Robinhood::Equity::Account::Portfolio');
}
=head2 C<equity_watchlists( )>
my $watchlists = $rh->equity_watchlists();
An iterator containing Finance::Robinhood::Equity::Watchlist objects is
returned. You need to be logged in for this to work.
=cut
sub equity_watchlists ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/watchlists/',
_class => 'Finance::Robinhood::Equity::Watchlist'
);
}
sub _test_equity_watchlists {
my $rh = t::Utility::rh_instance(1);
my $watchlists = $rh->equity_watchlists;
isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($watchlists->current, 'Finance::Robinhood::Equity::Watchlist');
}
=head2 C<equity_watchlist_by_name( ... )>
my $watchlist = $rh->equity_watchlist_by_name('Default');
Returns a Finance::Robinhood::Equity::Watchlist object. You need to be logged
in for this to work.
=cut
sub equity_watchlist_by_name ($s, $name) {
require Finance::Robinhood::Equity::Watchlist; # Subclass of Iterator
Finance::Robinhood::Equity::Watchlist->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/watchlists/' . $name . '/',
_class => 'Finance::Robinhood::Equity::Watchlist::Element',
name => $name
);
}
sub _test_equity_watchlist_by_name {
my $rh = t::Utility::rh_instance(1);
my $watchlist = $rh->equity_watchlist_by_name('Default');
isa_ok($watchlist, 'Finance::Robinhood::Equity::Watchlist');
}
=head2 C<equity_fundamentals( )>
my $fundamentals = $rh->equity_fundamentals('MSFT', 'TSLA');
An iterator containing Finance::Robinhood::Equity::Fundamentals objects is
returned.
You do not need to be logged in for this to work.
=cut
sub equity_fundamentals ($s, @symbols_or_ids_or_urls) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://api.robinhood.com/fundamentals/')->query(
{ ( grep {
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i
} @symbols_or_ids_or_urls
)
? (grep {/^https?/i} @symbols_or_ids_or_urls)
? 'instruments'
: 'ids'
: 'symbols' => join(',', @symbols_or_ids_or_urls)
}
),
_class => 'Finance::Robinhood::Equity::Fundamentals'
);
}
sub _test_equity_fundamentals {
my $rh = t::Utility::rh_instance(1);
isa_ok($rh->equity_fundamentals('MSFT')->current,
'Finance::Robinhood::Equity::Fundamentals',);
isa_ok($rh->equity_fundamentals('50810c35-d215-4866-9758-0ada4ac79ffa')
->current,
'Finance::Robinhood::Equity::Fundamentals',
);
isa_ok(
$rh->equity_fundamentals(
'https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/'
)->current,
'Finance::Robinhood::Equity::Fundamentals',
);
}
=head2 C<equity_markets( )>
my $markets = $rh->equity_markets()->all;
Returns an iterator containing Finance::Robinhood::Equity::Market objects.
=cut
sub equity_markets ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/markets/',
_class => 'Finance::Robinhood::Equity::Market'
);
}
sub _test_equity_markets {
my $markets = t::Utility::rh_instance(0)->equity_markets;
isa_ok($markets, 'Finance::Robinhood::Utilities::Iterator');
skip_all('No equity markets found') if !$markets->has_next;
isa_ok($markets->current, 'Finance::Robinhood::Equity::Market');
}
=head2 C<equity_market_by_mic( )>
my $markets = $rh->equity_market_by_mic('XNAS'); # NASDAQ
Locates an exchange by its Market Identifier Code and returns a
Finance::Robinhood::Equity::Market object.
See also https://en.wikipedia.org/wiki/Market_Identifier_Code
=cut
sub equity_market_by_mic ($s, $mic) {
my $res = $s->_get('https://api.robinhood.com/markets/' . $mic . '/');
require Finance::Robinhood::Equity::Market if $res->is_success;
return $res->is_success
? Finance::Robinhood::Equity::Market->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_equity_market_by_mic {
isa_ok(t::Utility::rh_instance(0)->equity_market_by_mic('XNAS'),
'Finance::Robinhood::Equity::Market');
}
=head2 C<top_movers( [...] )>
my $instruments = $rh->top_movers( );
Returns an iterator containing members of the S&P 500 with large price changes
during market hours as Finance::Robinhood::Equity::Movers objects.
You may define whether or not you want the best or worst performing instruments
with the following option:
=over
=item C<direction> - C<up> or C<down>
$rh->top_movers( direction => 'up' );
Returns the best performing members. This is the default.
$rh->top_movers( direction => 'down' );
Returns the worst performing members.
=back
=cut
sub top_movers ($s, %filter) {
$filter{direction} //= 'up';
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://midlands.robinhood.com/movers/sp500/')
->query(\%filter),
_class => 'Finance::Robinhood::Equity::Mover'
);
}
sub _test_top_movers {
my $rh = t::Utility::rh_instance(0);
my $movers = $rh->top_movers;
isa_ok($movers, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($movers->current, 'Finance::Robinhood::Equity::Mover');
}
=head2 C<tags( ... )>
my $tags = $rh->tags( 'food', 'oil' );
Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
=cut
sub tags ($s, @slugs) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://midlands.robinhood.com/tags/')
->query({slugs => join ',', @slugs}),
_class => 'Finance::Robinhood::Equity::Tag'
);
}
sub _test_tags {
my $rh = t::Utility::rh_instance(0);
my $tags = $rh->tags('food');
isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
}
=head2 C<tags_discovery( ... )>
my $tags = $rh->tags_discovery( );
Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
=cut
sub tags_discovery ( $s ) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://midlands.robinhood.com/tags/discovery/'),
_class => 'Finance::Robinhood::Equity::Tag'
);
}
sub _test_tags_discovery {
my $rh = t::Utility::rh_instance(0);
my $tags = $rh->tags_discovery();
isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
}
=head2 C<tags_popular( ... )>
my $tags = $rh->tags_popular( );
Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
=cut
sub tags_popular ( $s ) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://midlands.robinhood.com/tags/discovery/'),
_class => 'Finance::Robinhood::Equity::Tag'
);
}
sub _test_tags_popular {
my $rh = t::Utility::rh_instance(0);
my $tags = $rh->tags_popular();
isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
}
=head2 C<tag( ... )>
my $tag = $rh->tag('food');
Locates a tag by its slug and returns a Finance::Robinhood::Equity::Tag object.
=cut
sub tag ($s, $slug) {
my $res
= $s->_get('https://midlands.robinhood.com/tags/tag/' . $slug . '/');
require Finance::Robinhood::Equity::Tag if $res->is_success;
return $res->is_success
? Finance::Robinhood::Equity::Tag->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_tag {
isa_ok(t::Utility::rh_instance(0)->tag('food'),
'Finance::Robinhood::Equity::Tag');
}
=head1 OPTIONS METHODS
=head2 C<options_chains( )>
my $chains = $rh->options_chains->all;
Returns an iterator containing chain elements.
my $equity = $rh->search('MSFT')->equity_instruments->[0]->options_chains->all;
You may limit the call by passing a list of options instruments or a list of
equity instruments.
=cut
sub options_chains ($s, @filter) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://api.robinhood.com/options/chains/')
->query(
{ ( grep {
ref $_ eq 'Finance::Robinhood::Equity::Instrument'
} @filter
)
? (equity_instrument_ids => [map { $_->id } @filter])
: (grep {
ref $_ eq 'Finance::Robinhood::Options::Instrument'
} @filter
) ? (ids => [map { $_->chain_id } @filter]) : ()
}
),
_class => 'Finance::Robinhood::Options::Chain'
);
}
sub _test_options_chains {
my $rh = t::Utility::rh_instance(0);
my $chains = $rh->options_chains;
isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
# Get by equity instrument
$chains = $rh->options_chains($rh->search('MSFT')->equity_instruments);
isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
is($chains->current->symbol, 'MSFT');
# Get by options instrument
my ($instrument) = $rh->search('MSFT')->equity_instruments;
my $options =
$rh->options_instruments(chain_id => $instrument->tradable_chain_id,
tradability => 'tradable');
$chains = $rh->options_chains($options->next);
isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
is($chains->current->symbol, 'MSFT');
}
=head2 C<options_instruments( )>
my $options = $rh->options_instruments();
Returns an iterator containing Finance::Robinhood::Options::Instrument objects.
my $options = $rh->options_instruments( state => 'active', type => 'put' );
You can filter the results several ways. All of them are optional.
=over
=item C<state> - C<active>, C<inactive>, or C<expired>
=item C<type> - C<call> or C<put>
=item C<expiration_dates> - comma separated list of days; format is YYYY-M-DD
=back
=cut
sub options_instruments ($s, %filters) {
#$filters{chain_id} = $filters{chain}->chain_id if $filters{chain};
# - ids - comma separated list of options ids (optional)
# - cursor - paginated list position (optional)
# - tradability - 'tradable' or 'untradable' (optional)
# - state - 'active', 'inactive', or 'expired' (optional)
# - type - 'put' or 'call' (optional)
# - expiration_dates - comma separated list of days (optional; YYYY-MM-DD)
# - chain_id - related options chain id (optional; UUID)
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://api.robinhood.com/options/instruments/')
->query(\%filters),
_class => 'Finance::Robinhood::Options::Instrument'
);
}
sub _test_options_instruments {
my $rh = t::Utility::rh_instance(1);
my $options =
$rh->options_instruments(
chain_id =>
$rh->equity_instrument_by_symbol('MSFT')->tradable_chain_id,
tradability => 'tradable'
);
isa_ok($options, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($options->next, 'Finance::Robinhood::Options::Instrument');
is($options->current->chain_symbol, 'MSFT');
}
=head1 UNSORTED
=head2 C<user( )>
my $me = $rh->user();
Returns a Finance::Robinhood::User object. You need to be logged in for this to
work.
=cut
sub user ( $s ) {
my $res = $s->_get('https://api.robinhood.com/user/');
require Finance::Robinhood::User if $res->is_success;
return $res->is_success
? Finance::Robinhood::User->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_user {
my $rh = t::Utility::rh_instance(1);
my $me = $rh->user();
isa_ok($me, 'Finance::Robinhood::User');
}
=head2 C<acats_transfers( )>
my $acats = $rh->acats_transfers();
An iterator containing Finance::Robinhood::ACATS::Transfer objects is returned.
You need to be logged in for this to work.
=cut
sub acats_transfers ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://api.robinhood.com/acats/',
_class => 'Finance::Robinhood::ACATS::Transfer'
);
}
sub _test_acats_transfers {
my $transfers = t::Utility::rh_instance(1)->acats_transfers;
isa_ok($transfers, 'Finance::Robinhood::Utilities::Iterator');
skip_all('No ACATS transfers found') if !$transfers->has_next;
isa_ok($transfers->current, 'Finance::Robinhood::ACATS::Transfer');
}
=head2 C<equity_positions( )>
my $positions = $rh->equity_positions( );
Returns the related paginated list object filled with
Finance::Robinhood::Equity::Position objects.
You must be logged in.
my $positions = $rh->equity_positions( nonzero => 1 );
You can filter and modify the results. All options are optional.
=over
=item C<nonzero> - true or false. Default is false
=item C<ordering> - list of equity instruments
=back
=cut
sub equity_positions ($s, %filters) {
$filters{nonzero} = !!$filters{nonzero} ? 'true' : 'false'
if defined $filters{nonzero};
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://api.robinhood.com/positions/')
->query(\%filters),
_class => 'Finance::Robinhood::Equity::Position'
);
}
sub _test_equity_positions {
my $positions = t::Utility::rh_instance(1)->equity_positions;
isa_ok($positions, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($positions->current, 'Finance::Robinhood::Equity::Position');
}
=head2 C<equity_earnings( ... )>
my $earnings = $rh->equity_earnings( symbol => 'MSFT' );
Returns the related paginated list object filled with
Finance::Robinhood::Equity::Earnings objects by ticker symbol.
my $earnings = $rh->equity_earnings( instrument => $rh->equity_instrument_by_symbol('MSFT') );
Returns the related paginated list object filled with
Finance::Robinhood::Equity::Earnings objects by instrument object/url.
my $earnings = $rh->equity_earnings( range=> 7 );
Returns a paginated list object filled with
Finance::Robinhood::Equity::Earnings objects for all expected earnings report
over the next C<X> days where C<X> is between C<-21...-1, 1...21>. Negative
values are days into the past. Positive are days into the future.
You must be logged in for any of these to work.
=cut
sub equity_earnings ($s, %filters) {
$filters{range} = $filters{range} . 'day'
if defined $filters{range} && $filters{range} =~ m[^\-?\d+$];
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://api.robinhood.com/marketdata/earnings/')
->query(\%filters),
_class => 'Finance::Robinhood::Equity::Earnings'
);
}
sub _test_equity_earnings {
my $by_instrument
= t::Utility::rh_instance(1)
->equity_earnings(instrument =>
t::Utility::rh_instance(1)->equity_instrument_by_symbol('MSFT'));
isa_ok($by_instrument, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($by_instrument->current, 'Finance::Robinhood::Equity::Earnings');
is($by_instrument->current->symbol,
'MSFT', 'correct symbol (by instrument)');
#
my $by_symbol
= t::Utility::rh_instance(1)->equity_earnings(symbol => 'MSFT');
isa_ok($by_symbol, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($by_symbol->current, 'Finance::Robinhood::Equity::Earnings');
is($by_symbol->current->symbol, 'MSFT', 'correct symbol (by symbol)');
# Positive range
my $p_range = t::Utility::rh_instance(1)->equity_earnings(range => 7);
isa_ok($p_range, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($p_range->current, 'Finance::Robinhood::Equity::Earnings');
# Negative range
my $n_range = t::Utility::rh_instance(1)->equity_earnings(range => -7);
isa_ok($n_range, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($n_range->current, 'Finance::Robinhood::Equity::Earnings');
}
=head1 FOREX METHODS
Depending on your jurisdiction, your account may have access to Robinhood
Crypto. See https://crypto.robinhood.com/ for more.
=head2 C<forex_accounts( )>
my $halts = $rh->forex_accounts;
Returns an iterator full of Finance::Robinhood::Forex::Account objects.
You need to be logged in and have access to Robinhood Crypto for this to work.
=cut
sub forex_accounts( $s ) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page =>
Mojo::URL->new('https://nummus.robinhood.com/accounts/'),
_class => 'Finance::Robinhood::Forex::Account'
);
}
sub _test_forex_accounts {
my $halts = t::Utility::rh_instance(1)->forex_accounts;
isa_ok($halts, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($halts->current, 'Finance::Robinhood::Forex::Account');
}
=head2 C<forex_account_by_id( ... )>
my $account = $rh->forex_account_by_id($id);
Returns a Finance::Robinhood::Forex::Account object. You need to be logged in
for this to work.
=cut
sub forex_account_by_id ($s, $id) {
my $res = $s->_get('https://nummus.robinhood.com/accounts/' . $id . '/');
require Finance::Robinhood::Forex::Account if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Account->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_account_by_id {
my $rh = t::Utility::rh_instance(1);
my $acct = $rh->forex_account_by_id($rh->forex_accounts->current->id);
isa_ok($acct, 'Finance::Robinhood::Forex::Account');
}
=head2 C<forex_halts( [...] )>
my $halts = $rh->forex_halts;
# or
$halts = $rh->forex_halts( active => 1 );
Returns an iterator full of Finance::Robinhood::Forex::Halt objects.
If you pass a true value to a key named C<active>, only active halts will be
returned.
You need to be logged in and have access to Robinhood Crypto for this to work.
=cut
sub forex_halts ($s, %filters) {
$filters{active} = $filters{active} ? 'true' : 'false'
if defined $filters{active};
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://nummus.robinhood.com/halts/')
->query(\%filters),
_class => 'Finance::Robinhood::Forex::Halt'
);
}
sub _test_forex_halts {
my $halts = t::Utility::rh_instance(1)->forex_halts;
isa_ok($halts, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($halts->current, 'Finance::Robinhood::Forex::Halt');
#
is( scalar $halts->all
> scalar t::Utility::rh_instance(1)->forex_halts(active => 1)
->all,
1,
'active => 1 works'
);
}
=head2 C<forex_currencies( )>
my $currecies = $rh->forex_currencies();
An iterator containing Finance::Robinhood::Forex::Currency objects is returned.
You need to be logged in for this to work.
=cut
sub forex_currencies ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://nummus.robinhood.com/currencies/',
_class => 'Finance::Robinhood::Forex::Currency'
);
}
sub _test_forex_currencies {
my $rh = t::Utility::rh_instance(1);
my $currencies = $rh->forex_currencies;
isa_ok($currencies, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($currencies->current, 'Finance::Robinhood::Forex::Currency');
}
=head2 C<forex_currency_by_id( ... )>
my $currency = $rh->forex_currency_by_id($id);
Returns a Finance::Robinhood::Forex::Currency object. You need to be logged in
for this to work.
=cut
sub forex_currency_by_id ($s, $id) {
my $res
= $s->_get('https://nummus.robinhood.com/currencies/' . $id . '/');
require Finance::Robinhood::Forex::Currency if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Currency->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_currency_by_id {
my $rh = t::Utility::rh_instance(1);
my $usd
= $rh->forex_currency_by_id('1072fc76-1862-41ab-82c2-485837590762');
isa_ok($usd, 'Finance::Robinhood::Forex::Currency');
}
=head2 C<forex_pairs( )>
my $pairs = $rh->forex_pairs();
An iterator containing Finance::Robinhood::Forex::Pair objects is returned. You
need to be logged in for this to work.
=cut
sub forex_pairs ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://nummus.robinhood.com/currency_pairs/',
_class => 'Finance::Robinhood::Forex::Pair'
);
}
sub _test_forex_pairs {
my $rh = t::Utility::rh_instance(1);
my $watchlists = $rh->forex_pairs;
isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Pair');
}
=head2 C<forex_pair_by_id( ... )>
my $watchlist = $rh->forex_pair_by_id($id);
Returns a Finance::Robinhood::Forex::Pair object. You need to be logged in for
this to work.
=cut
sub forex_pair_by_id ($s, $id) {
my $res = $s->_get(
'https://nummus.robinhood.com/currency_pairs/' . $id . '/');
require Finance::Robinhood::Forex::Pair if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Pair->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_pair_by_id {
my $rh = t::Utility::rh_instance(1);
my $btc_usd
= $rh->forex_pair_by_id('3d961844-d360-45fc-989b-f6fca761d511')
; # BTC-USD
isa_ok($btc_usd, 'Finance::Robinhood::Forex::Pair');
}
=head2 C<forex_pair_by_symbol( ... )>
my $btc = $rh->forex_pair_by_symbol('BTCUSD');
Returns a Finance::Robinhood::Forex::Pair object. You need to be logged in for
this to work.
=cut
sub forex_pair_by_symbol ($s, $id) {
my $res = $s->_get(
'https://nummus.robinhood.com/currency_pairs/?symbols=' . $id);
require Finance::Robinhood::Forex::Pair if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Pair->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_pair_by_symbol {
my $rh = t::Utility::rh_instance(1);
my $btc_usd = $rh->forex_pair_by_symbol('BTCUSD'); # BTC-USD
isa_ok($btc_usd, 'Finance::Robinhood::Forex::Pair');
}
=head2 C<forex_watchlists( )>
my $watchlists = $rh->forex_watchlists();
An iterator containing Finance::Robinhood::Forex::Watchlist objects is
returned. You need to be logged in for this to work.
=cut
sub forex_watchlists ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://nummus.robinhood.com/watchlists/',
_class => 'Finance::Robinhood::Forex::Watchlist'
);
}
sub _test_forex_watchlists {
my $rh = t::Utility::rh_instance(1);
my $watchlists = $rh->forex_watchlists;
isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Watchlist');
}
=head2 C<forex_watchlist_by_id( ... )>
my $watchlist = $rh->forex_watchlist_by_id($id);
Returns a Finance::Robinhood::Forex::Watchlist object. You need to be logged in
for this to work.
=cut
sub forex_watchlist_by_id ($s, $id) {
my $res
= $s->_get('https://nummus.robinhood.com/watchlists/' . $id . '/');
require Finance::Robinhood::Forex::Watchlist if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Watchlist->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_watchlist_by_id {
my $rh = t::Utility::rh_instance(1);
my $watchlist
= $rh->forex_watchlist_by_id($rh->forex_watchlists->current->id);
isa_ok($watchlist, 'Finance::Robinhood::Forex::Watchlist');
}
=head2 C<forex_activations( )>
my $activations = $rh->forex_activations();
An iterator containing Finance::Robinhood::Forex::Activation objects is
returned. You need to be logged in for this to work.
=cut
sub forex_activations ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://nummus.robinhood.com/activations/',
_class => 'Finance::Robinhood::Forex::Activation'
);
}
sub _test_forex_activations {
my $rh = t::Utility::rh_instance(1);
my $watchlists = $rh->forex_activations;
isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Activation');
}
=head2 C<forex_activation_by_id( ... )>
my $activation = $rh->forex_activation_by_id($id);
Returns a Finance::Robinhood::Forex::Activation object. You need to be logged
in for this to work.
=cut
sub forex_activation_by_id ($s, $id) {
my $res
= $s->_get('https://nummus.robinhood.com/activations/' . $id . '/');
require Finance::Robinhood::Forex::Activation if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Activation->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_activation_by_id {
my $rh = t::Utility::rh_instance(1);
my $activation = $rh->forex_activations->current;
my $forex = $rh->forex_activation_by_id($activation->id); # Cheat
isa_ok($forex, 'Finance::Robinhood::Forex::Activation');
}
=head2 C<forex_portfolios( )>
my $portfolios = $rh->forex_portfolios();
An iterator containing Finance::Robinhood::Forex::Portfolio objects is
returned. You need to be logged in for this to work.
=cut
sub forex_portfolios ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => 'https://nummus.robinhood.com/portfolios/',
_class => 'Finance::Robinhood::Forex::Portfolio'
);
}
sub _test_forex_portfolios {
my $rh = t::Utility::rh_instance(1);
my $portfolios = $rh->forex_portfolios;
isa_ok($portfolios, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($portfolios->current, 'Finance::Robinhood::Forex::Portfolio');
}
=head2 C<forex_portfolio_by_id( ... )>
my $portfolio = $rh->forex_portfolio_by_id($id);
Returns a Finance::Robinhood::Forex::Portfolio object. You need to be logged in
for this to work.
=cut
sub forex_portfolio_by_id ($s, $id) {
my $res
= $s->_get('https://nummus.robinhood.com/portfolios/' . $id . '/');
require Finance::Robinhood::Forex::Portfolio if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Portfolio->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_portfolio_by_id {
my $rh = t::Utility::rh_instance(1);
my $portfolio = $rh->forex_portfolios->current;
my $forex = $rh->forex_portfolio_by_id($portfolio->id); # Cheat
isa_ok($forex, 'Finance::Robinhood::Forex::Portfolio');
}
=head2 C<forex_activation_request( ... )>
my $activation = $rh->forex_activation_request( type => 'new_account' );
Submits an application to activate a new forex account. If successful, a new
Fiance::Robinhood::Forex::Activation object is returned. You need to be logged
in for this to work.
The following options are accepted:
=over
=item C<type>
This is required and must be one of the following:
=over
=item C<new_account>
=item C<reactivation>
=back
=item C<speculative>
This is an optional boolean value.
=back
=cut
sub forex_activation_request ($s, %filters) {
$filters{type} = $filters{type} ? 'true' : 'false'
if defined $filters{type};
my $res = $s->_post('https://nummus.robinhood.com/activations/')
->query(\%filters);
require Finance::Robinhood::Forex::Activation if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Activation->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_activation_request {
diag(
'This is one of those methods that is almost impossible to test from this side.'
);
pass(
'Rather not have a million activation attempts attached to my account'
);
}
=head2 C<forex_orders( )>
my $orders = $rh->forex_orders( );
An iterator containing Finance::Robinhood::Forex::Order objects is returned.
You need to be logged in for this to work.
=cut
sub forex_orders ($s) {
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://nummus.robinhood.com/orders/'),
_class => 'Finance::Robinhood::Forex::Order'
);
}
sub _test_forex_orders {
my $rh = t::Utility::rh_instance(1);
my $orders = $rh->forex_orders;
isa_ok($orders, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($orders->current, 'Finance::Robinhood::Forex::Order');
}
=head2 C<forex_order_by_id( ... )>
my $order = $rh->forex_order_by_id($id);
Returns a Finance::Robinhood::Forex::Order object. You need to be logged in for
this to work.
=cut
sub forex_order_by_id ($s, $id) {
my $res = $s->_get('https://nummus.robinhood.com/orders/' . $id . '/');
require Finance::Robinhood::Forex::Order if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Order->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_order_by_id {
my $rh = t::Utility::rh_instance(1);
my $order = $rh->forex_orders->current;
my $forex = $rh->forex_order_by_id($order->id); # Cheat
isa_ok($forex, 'Finance::Robinhood::Forex::Order');
}
=head2 C<forex_holdings( )>
my $holdings = $rh->forex_holdings( );
Returns the related paginated list object filled with
Finance::Robinhood::Forex::Holding objects.
You must be logged in.
my $holdings = $rh->forex_holdings( nonzero => 1 );
You can filter and modify the results. All options are optional.
=over
=item C<nonzero> - true or false. Default is false.
=back
=cut
sub forex_holdings ($s, %filters) {
$filters{nonzero} = !!$filters{nonzero} ? 'true' : 'false'
if defined $filters{nonzero};
Finance::Robinhood::Utilities::Iterator->new(
_rh => $s,
_next_page => Mojo::URL->new('https://nummus.robinhood.com/holdings/')
->query(\%filters),
_class => 'Finance::Robinhood::Forex::Holding'
);
}
sub _test_forex_holdings {
my $positions = t::Utility::rh_instance(1)->forex_holdings;
isa_ok($positions, 'Finance::Robinhood::Utilities::Iterator');
isa_ok($positions->current, 'Finance::Robinhood::Forex::Holding');
}
=head2 C<forex_holding_by_id( ... )>
my $holding = $rh->forex_holding_by_id($id);
Returns a Finance::Robinhood::Forex::Holding object. You need to be logged in
for this to work.
=cut
sub forex_holding_by_id ($s, $id) {
my $res = $s->_get('https://nummus.robinhood.com/holdings/' . $id . '/');
require Finance::Robinhood::Forex::Holding if $res->is_success;
return $res->is_success
? Finance::Robinhood::Forex::Holding->new(_rh => $s, %{$res->json})
: Finance::Robinhood::Error->new(
$res->is_server_error ? (details => $res->message) : $res->json);
}
sub _test_forex_holding_by_id {
my $rh = t::Utility::rh_instance(1);
my $holding = $rh->forex_holding_by_id($rh->forex_holdings->current->id);
isa_ok($holding, 'Finance::Robinhood::Forex::Holding');
}
=head1 LEGAL
This is a simple wrapper around the API used in the official apps. The author
provides no investment, legal, or tax advice and is not responsible for any
damages incurred while using this software. This software is not affiliated
with Robinhood Financial LLC in any way.
For Robinhood's terms and disclosures, please see their website at
https://robinhood.com/legal/
=head1 LICENSE
Copyright (C) Sanko Robinson.
This library is free software; you can redistribute it and/or modify it under
the terms found in the Artistic License 2. Other copyrights, terms, and
conditions may apply to data transmitted through this module. Please refer to
the L<LEGAL> section.
=head1 AUTHOR
Sanko Robinson E<lt>sanko@cpan.orgE<gt>
=cut
1;