File Coverage

lib/Finance/Robinhood.pm
Criterion Covered Total %
statement 281 637 44.1
branch 27 194 13.9
condition 10 53 18.8
subroutine 83 122 68.0
pod 50 50 100.0
total 451 1056 42.7


line stmt bran cond sub pod time code
1             package Finance::Robinhood;
2              
3             =encoding utf-8
4              
5             =for stopwords watchlist watchlists untradable urls forex
6              
7             =head1 NAME
8              
9             Finance::Robinhood - Trade Stocks, ETFs, Options, and Cryptocurrency without
10             Commission
11              
12             =head1 SYNOPSIS
13              
14             use Finance::Robinhood;
15             my $rh = Finance::Robinhood->new();
16              
17             =cut
18              
19             our $VERSION = '0.92_003';
20             #
21 1     1   6489 use Mojo::Base-base, -signatures;
  1         131903  
  1         7  
22 1     1   4270 use Mojo::UserAgent;
  1         201990  
  1         8  
23 1     1   40 use Mojo::URL;
  1         2  
  1         3  
24             #
25 1     1   493 use Finance::Robinhood::Error;
  1         2  
  1         5  
26 1     1   366 use Finance::Robinhood::Utilities qw[gen_uuid];
  1         3  
  1         51  
27 1     1   392 use Finance::Robinhood::Utilities::Iterator;
  1         13  
  1         7  
28 1     1   376 use Finance::Robinhood::OAuth2::Token;
  1         2  
  1         6  
29              
30             =head1 METHODS
31              
32             Finance::Robinhood wraps several APIs. There are parts of this package that
33             will not apply because your account does not have access to certain features.
34              
35             =head2 C
36              
37             Robinhood requires an authorization token for most API calls. To get this
38             token, you must log in with your username and password. But we'll get into that
39             later. For now, let's create a client object...
40              
41             # You can look up some basic instrument data with this
42             my $rh = Finance::Robinhood->new();
43              
44             A new Finance::Robinhood object is created without credentials. Before you can
45             buy or sell or do almost anything else, you must L.
46              
47             =head3 C ...>
48              
49             If you have previously authorized this package to access your account, passing
50             the OAuth2 tokens here will prevent you from having to C with
51             your user data.
52              
53             These tokens should be kept private.
54              
55             =head3 C ...>
56              
57             If you have previously authorized this package to access your account, passing
58             the assigned device ID here will prevent you from having to authorize it again
59             upon C.
60              
61             Like authorization tokens, this UUID should be kept private.
62              
63             =cut
64              
65             has _ua => sub {
66             my $x = Mojo::UserAgent->new;
67             $x->transactor->name(sprintf 'Perl/%s (%s) %s/%s',
68             ($^V =~ m[([\.\d]+)]),
69             $^O, __PACKAGE__, $VERSION);
70             $x;
71             };
72              
73             BEGIN { # Hide giant UA from Data::Dump
74             Data::Dump::Filtered::add_dump_filter(
75             sub {
76 0         0 my ($ctx, $object) = @_;
77 0 0 0     0 $ctx->is_blessed &&
78             $ctx->class eq 'Mojo::UserAgent'
79             ? {'dump' => 'Mojo::UserAgent object {' .
80             $object->transactor->name . '}'
81             }
82             : ();
83             }
84 1 50 33 1   8497 ) if $Data::Dump::VERSION && require Data::Dump::Filtered;
85             }
86             has 'oauth2_token';
87             has 'device_token' => sub { gen_uuid() };
88              
89             sub _test_new {
90 1     1   1988 ok(t::Utility::rh_instance(1));
91             }
92              
93 188     188   2102 sub _get ($s, $url, %data) {
  188         465  
  188         436  
  188         458  
  188         251  
94 0         0 $data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{$data{$_}} : $data{$_}
95 188 50       805 for keys %data;
96 188         1466 $url = Mojo::URL->new($url);
97 188         44265 $url->query(\%data);
98              
99             #warn 'GET ' . $url;
100             #warn ' Auth: ' . (
101             # ( $s->oauth2_token && $url =~ m[^https://[a-z]+\.robinhood\.com/.+$] ) ? $s->oauth2_token->token_type :
102             # 'none' );
103 188 50 33     7339 my $retval = $s->_ua->get(
104             $url => {
105             ($s->oauth2_token &&
106             $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
107             )
108             ? ('Authorization' => ucfirst join ' ',
109             $s->oauth2_token->token_type, $s->oauth2_token->access_token
110             )
111             : ()
112             }
113             );
114              
115             #use Data::Dump;
116             #warn ' Result: ' . $retval->res->code;
117             #die if $retval->res->code == 401;
118             #use Data::Dump;
119             #ddx $retval->res->headers;
120             #ddx $retval;
121             #warn $retval->res->code;
122             #ddx $retval->res;
123             #warn $retval->res->body;
124 188 50 33     93920924 return $s->_get($url, %data)
125             if $retval->res->code == 401 && $s->_refresh_login_token;
126 188         3253 $retval->result;
127             }
128              
129             sub _test_get {
130 1     1   1866 my $rh = t::Utility::rh_instance(0);
131 1         11 my $res = $rh->_get('https://jsonplaceholder.typicode.com/todos/1');
132 1         31 isa_ok($res, 'Mojo::Message::Response');
133 1         315 is($res->json->{title}, 'delectus aut autem', '_post(...) works!');
134             #
135 1         845 $res = $rh->_get('https://httpstat.us/500');
136 1         35 isa_ok($res, 'Mojo::Message::Response');
137 1         397 ok(!$res->is_success);
138             }
139              
140 1     1   2 sub _options ($s, $url, %data) {
  1         2  
  1         2  
  1         2  
  1         2  
141 1 50 33     5 my $retval = $s->_ua->options(
142             Mojo::URL->new($url) => {
143             ($s->oauth2_token &&
144             $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
145             )
146             ? ('Authorization' => ucfirst join ' ',
147             $s->oauth2_token->token_type, $s->oauth2_token->access_token
148             )
149             : ()
150             } => json => \%data
151             );
152 1 50 33     176687 return $s->_options($url, %data)
153             if $retval->res->code == 401 && $s->_refresh_login_token;
154 1         17 $retval->result;
155             }
156              
157             sub _test_options {
158 1     1   1880 my $rh = t::Utility::rh_instance(0);
159 1         11 my $res = $rh->_options('https://jsonplaceholder.typicode.com/');
160 1         31 isa_ok($res, 'Mojo::Message::Response');
161 1         433 is($res->json, ());
162             }
163              
164 1     1   2 sub _post ($s, $url, %data) {
  1         3  
  1         3  
  1         4  
  1         3  
165              
166             #warn 'POST ' . $url;
167             #use Data::Dump;
168             #ddx \%data;
169             #$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
170             #warn ' Auth: ' . (($s->oauth2_token && $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]) ? $s->oauth2_token->token_type : 'none');
171             my $retval = $s->_ua->post(
172             Mojo::URL->new($url) => {
173             ($s->oauth2_token &&
174             $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
175             ) &&
176             !delete $data{'no_auth_token'}
177             ? ('Authorization' => ucfirst join ' ',
178             $s->oauth2_token->token_type, $s->oauth2_token->access_token
179             )
180             : (),
181             ($data{challenge_id}
182             ? ('X-Robinhood-Challenge-Response-ID' =>
183             delete $data{challenge_id})
184 1 50 33     7 : ()
    50          
185             )
186             } => json => \%data
187             );
188              
189             #use Data::Dump;
190             #warn ' Result: ' . $retval->res->code;
191             #die if $retval->res->code == 401;
192             #use Data::Dump;
193             #ddx $retval->res->headers;
194             #ddx $retval;
195             #warn $retval->res->code;
196             #ddx $retval->res;
197             #warn $retval->res->body;
198 1 50 33     206464 return $s->_post($url, %data) # Retry with new auth info
199             if $retval->res->code == 401 && $s->_refresh_login_token;
200 1         17 $retval->res;
201             }
202              
203             sub _test_post {
204 1     1   3321 my $rh = t::Utility::rh_instance(0);
205 1         14 my $res = $rh->_post('https://jsonplaceholder.typicode.com/posts/',
206             title => 'Whoa',
207             body => 'This is a test',
208             userId => 13
209             );
210 1         17 isa_ok($res, 'Mojo::Message::Response');
211 1         476 is($res->json,
212             {body => 'This is a test', title => 'Whoa', userId => 13, id => 101});
213             }
214              
215 2     2   6 sub _patch ($s, $url, %data) {
  2         3  
  2         4  
  2         7  
  2         2  
216              
217             #$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
218             my $retval = $s->_ua->patch(
219             Mojo::URL->new($url) => {
220             ($s->oauth2_token &&
221             $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
222             ) &&
223 2 50 33     8 !delete $data{'no_auth_token'}
224             ? ('Authorization' => ucfirst join ' ',
225             $s->oauth2_token->token_type, $s->oauth2_token->access_token
226             )
227             : ()
228             } => json => \%data
229             );
230 2 50 33     764786 return $s->_post($url, %data)
231             if $retval->res->code == 401 && $s->_refresh_login_token;
232 2         35 $retval->result;
233             }
234              
235             sub _test_patch {
236 1     1   2083 my $rh = t::Utility::rh_instance(0);
237 1         12 my $res = $rh->_patch('https://jsonplaceholder.typicode.com/posts/9/',
238             title => 'Updated');
239 1         38 isa_ok($res, 'Mojo::Message::Response');
240 1         441 is($res->json->{title}, 'Updated');
241             }
242              
243 0     0   0 sub _delete ($s, $url, %data) {
  0         0  
  0         0  
  0         0  
  0         0  
244              
245             #$data{$_} = ref $data{$_} eq 'ARRAY' ? join ',', @{ $data{$_} } : $data{$_} for keys %data;
246             my $retval = $s->_ua->delete(
247             Mojo::URL->new($url) => {
248             ($s->oauth2_token &&
249             $url =~ m[^https://[a-z]+\.robinhood\.com/.+$]
250             ) &&
251 0 0 0     0 !delete $data{'no_auth_token'}
252             ? ('Authorization' => ucfirst join ' ',
253             $s->oauth2_token->token_type, $s->oauth2_token->access_token
254             )
255             : ()
256             } => json => \%data
257             );
258 0 0 0     0 return $s->_delete($url, %data)
259             if $retval->res->code == 401 && $s->_refresh_login_token;
260 0         0 $retval->result;
261             }
262              
263             sub _test_delete {
264 1     1   1924 my $rh = t::Utility::rh_instance(0);
265 1         81 my $res = $rh->_patch('https://jsonplaceholder.typicode.com/posts/1/');
266 1         29 isa_ok($res, 'Mojo::Message::Response');
267 1         397 ok($res->is_success, 'Deleted'); # Lies
268             }
269              
270             =head2 C
271              
272             my $rh = Finance::Robinhood->new()->login($user, $pass);
273              
274             A new Finance::Robinhood object is created without credentials. Before you can
275             buy or sell or do almost anything else, you must L.
276              
277             =head3 C ...>
278              
279             my $rh = Finance::Robinhood->new()->login($user, $pass, mfa_callback => sub {
280             # Do something like pop open an inputbox in TK, read from shell or whatever
281             } );
282              
283             If you have MFA enabled, you may (or must) also pass a callback. When the code
284             is called, a ref will be passed that will contain C (a boolean
285             value) and C which might be C, C, etc. Your return value
286             must be the MFA code.
287              
288             =head3 C ...>
289              
290             my $rh = Finance::Robinhood->new()->login($user, $pass, mfa_code => 980385);
291              
292             If you already know the MFA code (for example if you have MFA enabled through
293             an app), you can pass that code directly and log in.
294              
295             =head3 C ...>
296              
297             my $rh = Finance::Robinhood->new()->login($user, $pass, challenge_callback => sub {
298             my ($challenge) = @_;
299             # Do something like pop open an inputbox in TK, read from shell or whatever
300             $challenge->respond( ... );
301             $challenge;
302             } );
303              
304             When logging in with a new client, you are required to authorize it to access
305             your account.
306              
307             This callback should should expect a Finance::Robinhood::Error::Challenge
308             object and must return the object after validation.
309              
310             =head2 C
311              
312             my $token = $rh->device_token;
313             # Store it
314              
315             To prevent your client from having to be reauthorized to access your account
316             every time it is run, call this method which returns the device token which
317             should be passed to C.
318              
319             # Reload token from storage
320             my $device = ...;
321             $rh->device_token($device);
322              
323             To prevent your client from having to reauthorize every time it is run, call
324             this to reload the same ID.
325              
326             =head2 C
327              
328             my $token $rh->oauth2_token;
329             # Store it
330              
331             To prevent your client from having to log in every time it is run, call this
332             method which returns the authorization tokens which should be passed to C
333             ... )>.
334              
335             This method returns a Finance::Robinhood::OAuth2::Token object.
336              
337             # Load token object from storage
338             my $oauth = ...;
339             $rh->oauth2_token($token);
340              
341             Reload OAuth2 tokens. You can skip logging in with your username and password
342             if this is successful.
343              
344             This method expects a Finance::Robinhood::OAuth2::Token object.
345              
346             =cut
347              
348 0     0 1 0 sub login ($s, $u, $p, %opt) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
349              
350             # OAUTH2
351             my $res = $s->_post(
352             'https://api.robinhood.com/oauth2/token/',
353             no_auth_token => 1, # NO AUTH INFO SENT!
354             challenge_type => 'email',
355             ($opt{challenge_id} ? (challenge_id => $opt{challenge_id}) : ()),
356             device_token => $s->device_token,
357             expires_in => 86400,
358             scope => 'internal',
359             username => $u,
360             password => $p,
361             ($opt{mfa_code} ? (mfa_code => $opt{mfa_code}) : ()),
362             grant_type => ($opt{grant_type} // 'password'),
363             client_id => $opt{client_id} // sub {
364 0     0   0 my (@k, $c) = split //, shift;
365             map { # cheap and easy
366 0         0 unshift @k, pop @k;
  0         0  
367 0         0 $c .= chr(ord ^ ord $k[0]);
368             } split //,
369             "\aW];&Y55\35I[\a,6&>[5\34\36\f\2]]\$\x179L\\\x0B4<;,\"*&\5);";
370 0         0 $c;
371             }
372 0 0 0     0 ->(__PACKAGE__)
    0 0        
373             );
374 0 0       0 if ($res->is_success) {
    0          
375 0 0       0 if ($res->json->{mfa_required}) {
376             return $opt{mfa_callback}
377             ? Finance::Robinhood::Error->new(
378             description => 'You must pass an mfa_callback.')
379             : $s->login($u, $p, %opt,
380 0 0       0 mfa_code => $opt{mfa_callback}->($res->json));
381             }
382             else {
383 0         0 require Finance::Robinhood::OAuth2::Token;
384 0         0 $s->oauth2_token(
385             Finance::Robinhood::OAuth2::Token->new($res->json));
386             }
387             }
388             elsif ($res->json->{challenge}) { # 400
389 0         0 require Finance::Robinhood::Error::Challenge;
390             return Finance::Robinhood::Error->new(
391             description => 'You must pass a challenge_callback.')
392 0 0       0 if !$opt{challenge_callback};
393             my $challenge =
394             $opt{challenge_callback}->(
395             Finance::Robinhood::Error::Challenge->new(
396             _rh => $s,
397 0         0 %{$res->json->{challenge}}
  0         0  
398             )
399             ); # Call it
400 0 0       0 return $challenge
401             ? $s->login($u, $p, %opt, challenge_id => $challenge->id)
402             : $challenge;
403             }
404             else {
405 0 0       0 return Finance::Robinhood::Error->new(
406             $res->is_server_error ? (details => $res->message) : $res->json);
407             }
408 0         0 $s;
409             }
410              
411             sub _test_login {
412 1     1   2733 my $rh = t::Utility::rh_instance(1);
413 0         0 isa_ok($rh->oauth2_token, 'Finance::Robinhood::OAuth2::Token');
414             }
415              
416             # Cannot test this without using the same token for 24hrs and letting it expire
417 0         0 sub _refresh_login_token ($s, %opt)
418 0     0   0 { # TODO: Store %opt from login and reuse it here
  0         0  
  0         0  
419 0   0     0 $s->oauth2_token // return; # OAUTH2
420             my $res = $s->_post(
421             'https://api.robinhood.com/oauth2/token/',
422             no_auth_token => 1, # NO AUTH INFO SENT!
423             scope => 'internal',
424             refresh_token => $s->oauth2_token->refresh_token,
425             grant_type => ($opt{grant_type} // 'password'),
426             client_id => $opt{client_id} // sub {
427 0     0   0 my (@k, $c) = split //, shift;
428             map { # cheap and easy
429 0         0 unshift @k, pop @k;
  0         0  
430 0         0 $c .= chr(ord ^ ord $k[0]);
431             } split //,
432             "\aW];&Y55\35I[\a,6&>[5\34\36\f\2]]\$\x179L\\\x0B4<;,\"*&\5);";
433 0         0 $c;
434             }
435 0   0     0 ->(__PACKAGE__),
      0        
436             );
437 0 0       0 if ($res->is_success) {
438 0         0 require Finance::Robinhood::OAuth2::Token;
439 0         0 $s->oauth2_token(Finance::Robinhood::OAuth2::Token->new($res->json));
440             }
441             else {
442 0 0       0 return Finance::Robinhood::Error->new(
443             $res->is_server_error ? (details => $res->message) : $res->json);
444             }
445 0         0 $s;
446             }
447              
448             =head2 C
449              
450             my $results = $rh->search('microsoft');
451              
452             Returns a set of search results as a Finance::Robinhood::Search object.
453              
454             You do not need to be logged in for this to work.
455              
456             =cut
457              
458 2     2 1 6 sub search ($s, $keyword) {
  2         4  
  2         3  
  2         3  
459 2         11 my $res = $s->_get('https://midlands.robinhood.com/search/',
460             query => $keyword);
461 2         941 require Finance::Robinhood::Search;
462             $res->is_success
463 2 0       14 ? Finance::Robinhood::Search->new(_rh => $s, %{$res->json})
  2 50       58  
464             : Finance::Robinhood::Error->new(
465             $res->is_server_error ? (details => $res->message) : $res->json);
466             }
467              
468             sub _test_search {
469 1     1   3901 my $rh = t::Utility::rh_instance(1);
470 0         0 isa_ok($rh->search('tesla'), 'Finance::Robinhood::Search');
471             }
472              
473             =head2 C
474              
475             my $news = $rh->news('MSFT');
476             my $news = $rh->news('1072fc76-1862-41ab-82c2-485837590762'); # Forex - USD
477              
478             An iterator containing Finance::Robinhood::News objects is returned.
479              
480             =cut
481              
482 3     3 1 20 sub news ($s, $symbol_or_id) {
  3         8  
  3         7  
  3         6  
483 3 100       21 Finance::Robinhood::Utilities::Iterator->new(
484             _rh => $s,
485             _next_page =>
486             Mojo::URL->new('https://midlands.robinhood.com/news/')->query(
487             { ( $symbol_or_id
488             =~ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
489             ? 'currency_id'
490             : 'symbol'
491             ) => $symbol_or_id
492             }
493             ),
494             _class => 'Finance::Robinhood::News'
495             );
496             }
497              
498             sub _test_news {
499 1     1   1870 my $rh = t::Utility::rh_instance();
500 1         78 my $msft = $rh->news('MSFT');
501 1         290 isa_ok($msft, 'Finance::Robinhood::Utilities::Iterator');
502 1 50       293 $msft->has_next
503             ? isa_ok($msft->next, 'Finance::Robinhood::News')
504             : pass('Fake it... Might not be any news on the weekend');
505 1         466 my $btc = $rh->news('d674efea-e623-4396-9026-39574b92b093');
506 1         289 isa_ok($btc, 'Finance::Robinhood::Utilities::Iterator');
507 1 50       295 $btc->has_next
508             ? isa_ok($btc->next, 'Finance::Robinhood::News')
509             : pass('Fake it... Might not be any news on the weekend');
510             }
511              
512             =head2 C
513              
514             my $feed = $rh->feed();
515              
516             An iterator containing Finance::Robinhood::News objects is returned. This list
517             will be filled with news related to instruments in your watchlist and
518             portfolio.
519              
520             You need to be logged in for this to work.
521              
522             =cut
523              
524 0     0 1 0 sub feed ($s) {
  0         0  
  0         0  
525 0         0 Finance::Robinhood::Utilities::Iterator->new(
526             _rh => $s,
527             _next_page => 'https://midlands.robinhood.com/feed/',
528             _class => 'Finance::Robinhood::News'
529             );
530             }
531              
532             sub _test_feed {
533 1     1   1914 my $feed = t::Utility::rh_instance(1)->feed;
534 0         0 isa_ok($feed, 'Finance::Robinhood::Utilities::Iterator');
535 0         0 isa_ok($feed->current, 'Finance::Robinhood::News');
536             }
537              
538             =head2 C
539              
540             my $cards = $rh->notifications();
541              
542             An iterator containing Finance::Robinhood::Notification objects is returned.
543              
544             You need to be logged in for this to work.
545              
546             =cut
547              
548 0     0 1 0 sub notifications ($s) {
  0         0  
  0         0  
549 0         0 Finance::Robinhood::Utilities::Iterator->new(
550             _rh => $s,
551             _next_page => 'https://midlands.robinhood.com/notifications/stack/',
552             _class => 'Finance::Robinhood::Notification'
553             );
554             }
555              
556             sub _test_notifications {
557 1     1   2020 my $cards = t::Utility::rh_instance(1)->notifications;
558 0         0 isa_ok($cards, 'Finance::Robinhood::Utilities::Iterator');
559 0         0 isa_ok($cards->current, 'Finance::Robinhood::Notification');
560             }
561              
562             =head2 C
563              
564             my $card = $rh->notification_by_id($id);
565              
566             Returns a Finance::Robinhood::Notification object. You need to be logged in for
567             this to work.
568              
569             =cut
570              
571 0     0 1 0 sub notification_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
572 0         0 my $res = $s->_get(
573             'https://midlands.robinhood.com/notifications/stack/' . $id . '/');
574 0 0       0 require Finance::Robinhood::Notification if $res->is_success;
575             return $res->is_success
576 0 0       0 ? Finance::Robinhood::Notification->new(_rh => $s, %{$res->json})
  0 0       0  
577             : Finance::Robinhood::Error->new(
578             $res->is_server_error ? (details => $res->message) : $res->json);
579             }
580              
581             sub _test_notification_by_id {
582 1     1   2917 my $rh = t::Utility::rh_instance(1);
583 0         0 my $card = $rh->notification_by_id($rh->notifications->current->id);
584 0         0 isa_ok($card, 'Finance::Robinhood::Notification');
585             }
586              
587             =head1 EQUITY METHODS
588              
589              
590             =head2 C
591              
592             my $instruments = $rh->equity_instruments();
593              
594             Returns an iterator containing equity instruments.
595              
596             You may restrict, search, or modify the list of instruments returned with the
597             following optional arguments:
598              
599             =over
600              
601             =item C - Ticker symbol
602              
603             my $msft = $rh->equity_instruments(symbol => 'MSFT')->next;
604              
605             By the way, C exists as sugar. It returns the
606             instrument itself rather than an iterator object with a single element.
607              
608             =item C - Keyword search
609              
610             my @solar = $rh->equity_instruments(query => 'solar')->all;
611              
612             =item C - List of instrument ids
613              
614             my ( $msft, $tsla )
615             = $rh->equity_instruments(
616             ids => [ '50810c35-d215-4866-9758-0ada4ac79ffa', 'e39ed23a-7bd1-4587-b060-71988d9ef483' ] )
617             ->all;
618              
619             If you happen to know/store instrument ids, quickly get full instrument objects
620             this way.
621              
622             =back
623              
624             =cut
625              
626 17     17 1 46 sub equity_instruments ($s, %filter) {
  17         43  
  17         55  
  17         28  
627 4         17 $filter{ids} = join ',', @{$filter{ids}}
628 17 100       71 if $filter{ids}; # Has to be done manually
629 17         138 Finance::Robinhood::Utilities::Iterator->new(
630             _rh => $s,
631             _next_page => Mojo::URL->new('https://api.robinhood.com/instruments/')
632             ->query(\%filter),
633             _class => 'Finance::Robinhood::Equity::Instrument'
634             );
635             }
636              
637             sub _test_equity_instruments {
638 1     1   2592 my $rh = t::Utility::rh_instance(0);
639 1         11 my $instruments = $rh->equity_instruments;
640 1         192 isa_ok($instruments, 'Finance::Robinhood::Utilities::Iterator');
641 1         272 isa_ok($instruments->next, 'Finance::Robinhood::Equity::Instrument');
642             #
643             {
644 1         7 my $msft = $rh->equity_instruments(symbol => 'MSFT')->current;
645 1         24 isa_ok($msft, 'Finance::Robinhood::Equity::Instrument');
646 1         439 is($msft->symbol, 'MSFT',
647             'equity_instruments(symbol => "MSFT") returned Microsoft');
648             }
649             #
650             {
651 1         441 my $tsla = $rh->equity_instruments(query => 'tesla')->current;
  1         7  
652 1         22 isa_ok($tsla, 'Finance::Robinhood::Equity::Instrument');
653 1         450 is($tsla->symbol, 'TSLA',
654             'equity_instruments(query => "tesla") returned Tesla');
655             }
656             {
657 1         592 my ($msft, $tsla)
  1         585  
  1         7  
658             = $rh->equity_instruments(
659             ids => [
660             '50810c35-d215-4866-9758-0ada4ac79ffa',
661             'e39ed23a-7bd1-4587-b060-71988d9ef483'
662             ]
663             )->all;
664 1         22 isa_ok($msft, 'Finance::Robinhood::Equity::Instrument');
665 1         429 is($msft->symbol, 'MSFT',
666             'equity_instruments( ids => ... ) returned Microsoft');
667 1         603 isa_ok($tsla, 'Finance::Robinhood::Equity::Instrument');
668 1         267 is($tsla->symbol, 'TSLA',
669             'equity_instruments( ids => ... ) also returned Tesla');
670             }
671             }
672              
673             =head2 C
674              
675             my $instrument = $rh->equity_instrument_by_symbol('MSFT');
676              
677             Searches for an equity instrument by ticker symbol and returns a
678             Finance::Robinhood::Equity::Instrument.
679              
680             =cut
681              
682 5     5 1 15 sub equity_instrument_by_symbol ($s, $symbol) {
  5         13  
  5         8  
  5         8  
683 5         28 $s->equity_instruments(symbol => $symbol)->current;
684             }
685              
686             sub _test_equity_instrument_by_symbol {
687 1     1   2556 my $rh = t::Utility::rh_instance(0);
688 1         11 my $instrument = $rh->equity_instrument_by_symbol('MSFT');
689 1         25 isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
690             }
691              
692             =head2 C
693              
694             my $instrument = $rh->equity_instrument_by_id('50810c35-d215-4866-9758-0ada4ac79ffa');
695              
696             Searches for a single of equity instrument by its instrument id and returns a
697             Finance::Robinhood::Equity::Instrument object.
698              
699             =cut
700              
701 1     1 1 2 sub equity_instrument_by_id ($s, $id) {
  1         3  
  1         1  
  1         3  
702 1         6 $s->equity_instruments(ids => [$id])->next();
703             }
704              
705             sub _test_equity_instrument_by_id {
706 1     1   1529 my $rh = t::Utility::rh_instance(0);
707 1         15 my $instrument = $rh->equity_instrument_by_id(
708             '50810c35-d215-4866-9758-0ada4ac79ffa');
709 1         30 isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
710 1         365 is($instrument->symbol, 'MSFT',
711             'equity_instruments( ids => ... ) returned Microsoft');
712             }
713              
714             =head2 C
715              
716             my $instrument = $rh->equity_instruments_by_id('50810c35-d215-4866-9758-0ada4ac79ffa');
717              
718             Searches for a list of equity instruments by their instrument ids and returns a
719             list of Finance::Robinhood::Equity::Instrument objects.
720              
721             =cut
722              
723 2     2 1 6 sub equity_instruments_by_id ($s, @ids) {
  2         5  
  2         7  
  2         4  
724              
725             # Split ids into groups of 75 to keep URL length down
726 2         5 my @retval;
727 2         20 push @retval, $s->equity_instruments(ids => [splice @ids, 0, 75])->all()
728             while @ids;
729 2         16 @retval;
730             }
731              
732             sub _test_equity_instruments_by_id {
733 1     1   3803 my $rh = t::Utility::rh_instance(0);
734 1         14 my ($instrument)
735             = $rh->equity_instruments_by_id(
736             '50810c35-d215-4866-9758-0ada4ac79ffa');
737 1         11 isa_ok($instrument, 'Finance::Robinhood::Equity::Instrument');
738 1         428 is($instrument->symbol, 'MSFT',
739             'equity_instruments( ids => ... ) returned Microsoft');
740             }
741              
742             =head2 C
743              
744             my $orders = $rh->equity_orders();
745              
746             An iterator containing Finance::Robinhood::Equity::Order objects is returned.
747             You need to be logged in for this to work.
748              
749             my $orders = $rh->equity_orders(instrument => $msft);
750              
751             If you would only like orders after a certain date, you can do that!
752              
753             my $orders = $rh->equity_orders(after => Time::Moment->now->minus_days(7));
754             # Also accepts ISO 8601
755              
756             If you would only like orders before a certain date, you can do that!
757              
758             my $orders = $rh->equity_orders(before => Time::Moment->now->minus_years(2));
759             # Also accepts ISO 8601
760              
761             =cut
762              
763 0     0 1 0 sub equity_orders ($s, %opts) {
  0         0  
  0         0  
  0         0  
764              
765             #- `updated_at[gte]` - greater than or equal to a date; timestamp or ISO 8601
766             #- `updated_at[lte]` - less than or equal to a date; timestamp or ISO 8601
767             #- `instrument` - equity instrument URL
768             Finance::Robinhood::Utilities::Iterator->new(
769             _rh => $s,
770             _next_page =>
771             Mojo::URL->new('https://api.robinhood.com/orders/')->query(
772             {$opts{instrument}
773             ? (instrument => $opts{instrument}->url)
774             : (),
775             $opts{before} ? ('updated_at[lte]' => +$opts{before}) : (),
776 0 0       0 $opts{after} ? ('updated_at[gte]' => +$opts{after}) : ()
    0          
    0          
777             }
778             ),
779             _class => 'Finance::Robinhood::Equity::Order'
780             );
781             }
782              
783             sub _test_equity_orders {
784 1     1   1968 my $rh = t::Utility::rh_instance(1);
785 0         0 my $orders = $rh->equity_orders;
786 0         0 isa_ok($orders, 'Finance::Robinhood::Utilities::Iterator');
787 0         0 isa_ok($orders->next, 'Finance::Robinhood::Equity::Order');
788             }
789              
790             =head2 C
791              
792             my $order = $rh->equity_order_by_id($id);
793              
794             Returns a Finance::Robinhood::Equity::Order object. You need to be logged in
795             for this to work.
796              
797             =cut
798              
799 0     0 1 0 sub equity_order_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
800 0         0 my $res = $s->_get('https://api.robinhood.com/orders/' . $id . '/');
801 0 0       0 require Finance::Robinhood::Equity::Order if $res->is_success;
802             return $res->is_success
803 0 0       0 ? Finance::Robinhood::Equity::Order->new(_rh => $s, %{$res->json})
  0 0       0  
804             : Finance::Robinhood::Error->new(
805             $res->is_server_error ? (details => $res->message) : $res->json);
806             }
807              
808             sub _test_equity_order_by_id {
809 1     1   2561 my $rh = t::Utility::rh_instance(1);
810 0         0 my $order = $rh->equity_order_by_id($rh->equity_orders->current->id);
811 0         0 isa_ok($order, 'Finance::Robinhood::Equity::Order');
812             }
813              
814             =head2 C
815              
816             my $accounts = $rh->equity_accounts();
817              
818             An iterator containing Finance::Robinhood::Equity::Account objects is returned.
819             You need to be logged in for this to work.
820              
821             =cut
822              
823 0     0 1 0 sub equity_accounts ($s) {
  0         0  
  0         0  
824 0         0 Finance::Robinhood::Utilities::Iterator->new(
825             _rh => $s,
826             _next_page => 'https://api.robinhood.com/accounts/',
827             _class => 'Finance::Robinhood::Equity::Account'
828             );
829             }
830              
831             sub _test_equity_accounts {
832 1     1   1608 my $rh = t::Utility::rh_instance(1);
833 0         0 my $accounts = $rh->equity_accounts;
834 0         0 isa_ok($accounts, 'Finance::Robinhood::Utilities::Iterator');
835 0         0 isa_ok($accounts->current, 'Finance::Robinhood::Equity::Account');
836             }
837              
838             =head2 C
839              
840             my $account = $rh->equity_account_by_account_number($id);
841              
842             Returns a Finance::Robinhood::Equity::Account object. You need to be logged in
843             for this to work.
844              
845             =cut
846              
847 0     0 1 0 sub equity_account_by_account_number ($s, $id) {
  0         0  
  0         0  
  0         0  
848 0         0 my $res = $s->_get('https://api.robinhood.com/accounts/' . $id . '/');
849 0 0       0 require Finance::Robinhood::Equity::Account if $res->is_success;
850             return $res->is_success
851 0 0       0 ? Finance::Robinhood::Equity::Account->new(_rh => $s, %{$res->json})
  0 0       0  
852             : Finance::Robinhood::Error->new(
853             $res->is_server_error ? (details => $res->message) : $res->json);
854             }
855              
856             sub _test_equity_account_by_account_number {
857 1     1   2188 my $rh = t::Utility::rh_instance(1);
858 0         0 my $acct = $rh->equity_account_by_account_number(
859             $rh->equity_accounts->current->account_number);
860 0         0 isa_ok($acct, 'Finance::Robinhood::Equity::Account');
861             }
862              
863             =head2 C
864              
865             my $equity_portfolios = $rh->equity_portfolios();
866              
867             An iterator containing Finance::Robinhood::Equity::Account::Portfolio objects
868             is returned. You need to be logged in for this to work.
869              
870             =cut
871              
872 0     0 1 0 sub equity_portfolios ($s) {
  0         0  
  0         0  
873 0         0 Finance::Robinhood::Utilities::Iterator->new(
874             _rh => $s,
875             _next_page => 'https://api.robinhood.com/portfolios/',
876             _class => 'Finance::Robinhood::Equity::Account::Portfolio'
877             );
878             }
879              
880             sub _test_equity_portfolios {
881 1     1   1886 my $rh = t::Utility::rh_instance(1);
882 0         0 my $equity_portfolios = $rh->equity_portfolios;
883 0         0 isa_ok($equity_portfolios, 'Finance::Robinhood::Utilities::Iterator');
884 0         0 isa_ok($equity_portfolios->current,
885             'Finance::Robinhood::Equity::Account::Portfolio');
886             }
887              
888             =head2 C
889              
890             my $watchlists = $rh->equity_watchlists();
891              
892             An iterator containing Finance::Robinhood::Equity::Watchlist objects is
893             returned. You need to be logged in for this to work.
894              
895             =cut
896              
897 0     0 1 0 sub equity_watchlists ($s) {
  0         0  
  0         0  
898 0         0 Finance::Robinhood::Utilities::Iterator->new(
899             _rh => $s,
900             _next_page => 'https://api.robinhood.com/watchlists/',
901             _class => 'Finance::Robinhood::Equity::Watchlist'
902             );
903             }
904              
905             sub _test_equity_watchlists {
906 1     1   1902 my $rh = t::Utility::rh_instance(1);
907 0         0 my $watchlists = $rh->equity_watchlists;
908 0         0 isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
909 0         0 isa_ok($watchlists->current, 'Finance::Robinhood::Equity::Watchlist');
910             }
911              
912             =head2 C
913              
914             my $watchlist = $rh->equity_watchlist_by_name('Default');
915              
916             Returns a Finance::Robinhood::Equity::Watchlist object. You need to be logged
917             in for this to work.
918              
919             =cut
920              
921 0     0 1 0 sub equity_watchlist_by_name ($s, $name) {
  0         0  
  0         0  
  0         0  
922 0         0 require Finance::Robinhood::Equity::Watchlist; # Subclass of Iterator
923 0         0 Finance::Robinhood::Equity::Watchlist->new(
924             _rh => $s,
925             _next_page => 'https://api.robinhood.com/watchlists/' . $name . '/',
926             _class => 'Finance::Robinhood::Equity::Watchlist::Element',
927             name => $name
928             );
929             }
930              
931             sub _test_equity_watchlist_by_name {
932 1     1   1874 my $rh = t::Utility::rh_instance(1);
933 0         0 my $watchlist = $rh->equity_watchlist_by_name('Default');
934 0         0 isa_ok($watchlist, 'Finance::Robinhood::Equity::Watchlist');
935             }
936              
937             =head2 C
938              
939             my $fundamentals = $rh->equity_fundamentals('MSFT', 'TSLA');
940              
941             An iterator containing Finance::Robinhood::Equity::Fundamentals objects is
942             returned.
943              
944             You do not need to be logged in for this to work.
945              
946             =cut
947              
948 0     0 1 0 sub equity_fundamentals ($s, @symbols_or_ids_or_urls) {
  0         0  
  0         0  
  0         0  
949             Finance::Robinhood::Utilities::Iterator->new(
950             _rh => $s,
951             _next_page =>
952             Mojo::URL->new('https://api.robinhood.com/fundamentals/')->query(
953             { ( grep {
954 0         0 /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i
955             } @symbols_or_ids_or_urls
956             )
957 0 0       0 ? (grep {/^https?/i} @symbols_or_ids_or_urls)
  0 0       0  
958             ? 'instruments'
959             : 'ids'
960             : 'symbols' => join(',', @symbols_or_ids_or_urls)
961             }
962             ),
963             _class => 'Finance::Robinhood::Equity::Fundamentals'
964             );
965             }
966              
967             sub _test_equity_fundamentals {
968 1     1   1517 my $rh = t::Utility::rh_instance(1);
969 0         0 isa_ok($rh->equity_fundamentals('MSFT')->current,
970             'Finance::Robinhood::Equity::Fundamentals',);
971 0         0 isa_ok($rh->equity_fundamentals('50810c35-d215-4866-9758-0ada4ac79ffa')
972             ->current,
973             'Finance::Robinhood::Equity::Fundamentals',
974             );
975 0         0 isa_ok(
976             $rh->equity_fundamentals(
977             'https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/'
978             )->current,
979             'Finance::Robinhood::Equity::Fundamentals',
980             );
981             }
982              
983             =head2 C
984              
985             my $markets = $rh->equity_markets()->all;
986              
987             Returns an iterator containing Finance::Robinhood::Equity::Market objects.
988              
989             =cut
990              
991 1     1 1 12 sub equity_markets ($s) {
  1         2  
  1         2  
992 1         8 Finance::Robinhood::Utilities::Iterator->new(
993             _rh => $s,
994             _next_page => 'https://api.robinhood.com/markets/',
995             _class => 'Finance::Robinhood::Equity::Market'
996             );
997             }
998              
999             sub _test_equity_markets {
1000 1     1   2479 my $markets = t::Utility::rh_instance(0)->equity_markets;
1001 1         33 isa_ok($markets, 'Finance::Robinhood::Utilities::Iterator');
1002 1 50       269 skip_all('No equity markets found') if !$markets->has_next;
1003 1         12 isa_ok($markets->current, 'Finance::Robinhood::Equity::Market');
1004             }
1005              
1006             =head2 C
1007              
1008             my $markets = $rh->equity_market_by_mic('XNAS'); # NASDAQ
1009              
1010             Locates an exchange by its Market Identifier Code and returns a
1011             Finance::Robinhood::Equity::Market object.
1012              
1013             See also https://en.wikipedia.org/wiki/Market_Identifier_Code
1014              
1015             =cut
1016              
1017 4     4 1 21 sub equity_market_by_mic ($s, $mic) {
  4         11  
  4         11  
  4         9  
1018 4         25 my $res = $s->_get('https://api.robinhood.com/markets/' . $mic . '/');
1019 4 50       125 require Finance::Robinhood::Equity::Market if $res->is_success;
1020             return $res->is_success
1021 4 0       133 ? Finance::Robinhood::Equity::Market->new(_rh => $s, %{$res->json})
  4 50       80  
1022             : Finance::Robinhood::Error->new(
1023             $res->is_server_error ? (details => $res->message) : $res->json);
1024             }
1025              
1026             sub _test_equity_market_by_mic {
1027 1     1   2856 isa_ok(t::Utility::rh_instance(0)->equity_market_by_mic('XNAS'),
1028             'Finance::Robinhood::Equity::Market');
1029             }
1030              
1031             =head2 C
1032              
1033             my $instruments = $rh->top_movers( );
1034              
1035             Returns an iterator containing members of the S&P 500 with large price changes
1036             during market hours as Finance::Robinhood::Equity::Movers objects.
1037              
1038             You may define whether or not you want the best or worst performing instruments
1039             with the following option:
1040              
1041             =over
1042              
1043             =item C - C or C
1044              
1045             $rh->top_movers( direction => 'up' );
1046              
1047             Returns the best performing members. This is the default.
1048              
1049             $rh->top_movers( direction => 'down' );
1050              
1051             Returns the worst performing members.
1052              
1053             =back
1054              
1055             =cut
1056              
1057 1     1 1 4 sub top_movers ($s, %filter) {
  1         3  
  1         2  
  1         2  
1058 1   50     10 $filter{direction} //= 'up';
1059 1         12 Finance::Robinhood::Utilities::Iterator->new(
1060             _rh => $s,
1061             _next_page =>
1062             Mojo::URL->new('https://midlands.robinhood.com/movers/sp500/')
1063             ->query(\%filter),
1064             _class => 'Finance::Robinhood::Equity::Mover'
1065             );
1066             }
1067              
1068             sub _test_top_movers {
1069 1     1   2822 my $rh = t::Utility::rh_instance(0);
1070 1         13 my $movers = $rh->top_movers;
1071 1         291 isa_ok($movers, 'Finance::Robinhood::Utilities::Iterator');
1072 1         275 isa_ok($movers->current, 'Finance::Robinhood::Equity::Mover');
1073             }
1074              
1075             =head2 C
1076              
1077             my $tags = $rh->tags( 'food', 'oil' );
1078              
1079             Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
1080              
1081             =cut
1082              
1083 1     1 1 3 sub tags ($s, @slugs) {
  1         3  
  1         4  
  1         2  
1084 1         9 Finance::Robinhood::Utilities::Iterator->new(
1085             _rh => $s,
1086             _next_page => Mojo::URL->new('https://midlands.robinhood.com/tags/')
1087             ->query({slugs => join ',', @slugs}),
1088             _class => 'Finance::Robinhood::Equity::Tag'
1089             );
1090             }
1091              
1092             sub _test_tags {
1093 1     1   2727 my $rh = t::Utility::rh_instance(0);
1094 1         15 my $tags = $rh->tags('food');
1095 1         297 isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
1096 1         283 isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
1097             }
1098              
1099             =head2 C
1100              
1101             my $tags = $rh->tags_discovery( );
1102              
1103             Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
1104              
1105             =cut
1106              
1107 1     1 1 2 sub tags_discovery ( $s ) {
  1         3  
  1         2  
1108 1         9 Finance::Robinhood::Utilities::Iterator->new(
1109             _rh => $s,
1110             _next_page =>
1111             Mojo::URL->new('https://midlands.robinhood.com/tags/discovery/'),
1112             _class => 'Finance::Robinhood::Equity::Tag'
1113             );
1114             }
1115              
1116             sub _test_tags_discovery {
1117 1     1   2909 my $rh = t::Utility::rh_instance(0);
1118 1         15 my $tags = $rh->tags_discovery();
1119 1         278 isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
1120 1         288 isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
1121             }
1122              
1123             =head2 C
1124              
1125             my $tags = $rh->tags_popular( );
1126              
1127             Returns an iterator containing Finance::Robinhood::Equity::Tag objects.
1128              
1129             =cut
1130              
1131 1     1 1 4 sub tags_popular ( $s ) {
  1         2  
  1         4  
1132 1         13 Finance::Robinhood::Utilities::Iterator->new(
1133             _rh => $s,
1134             _next_page =>
1135             Mojo::URL->new('https://midlands.robinhood.com/tags/discovery/'),
1136             _class => 'Finance::Robinhood::Equity::Tag'
1137             );
1138             }
1139              
1140             sub _test_tags_popular {
1141 1     1   2926 my $rh = t::Utility::rh_instance(0);
1142 1         63 my $tags = $rh->tags_popular();
1143 1         211 isa_ok($tags, 'Finance::Robinhood::Utilities::Iterator');
1144 1         273 isa_ok($tags->current, 'Finance::Robinhood::Equity::Tag');
1145             }
1146              
1147             =head2 C
1148              
1149             my $tag = $rh->tag('food');
1150              
1151             Locates a tag by its slug and returns a Finance::Robinhood::Equity::Tag object.
1152              
1153             =cut
1154              
1155 1     1 1 20 sub tag ($s, $slug) {
  1         3  
  1         3  
  1         2  
1156 1         9 my $res
1157             = $s->_get('https://midlands.robinhood.com/tags/tag/' . $slug . '/');
1158 1 50       33 require Finance::Robinhood::Equity::Tag if $res->is_success;
1159             return $res->is_success
1160 1 0       32 ? Finance::Robinhood::Equity::Tag->new(_rh => $s, %{$res->json})
  1 50       20  
1161             : Finance::Robinhood::Error->new(
1162             $res->is_server_error ? (details => $res->message) : $res->json);
1163             }
1164              
1165             sub _test_tag {
1166 1     1   2010 isa_ok(t::Utility::rh_instance(0)->tag('food'),
1167             'Finance::Robinhood::Equity::Tag');
1168             }
1169              
1170             =head1 OPTIONS METHODS
1171              
1172             =head2 C
1173              
1174             my $chains = $rh->options_chains->all;
1175              
1176             Returns an iterator containing chain elements.
1177              
1178             my $equity = $rh->search('MSFT')->equity_instruments->[0]->options_chains->all;
1179              
1180             You may limit the call by passing a list of options instruments or a list of
1181             equity instruments.
1182              
1183             =cut
1184              
1185 6     6 1 89 sub options_chains ($s, @filter) {
  6         17  
  6         13  
  6         15  
1186             Finance::Robinhood::Utilities::Iterator->new(
1187             _rh => $s,
1188             _next_page =>
1189             Mojo::URL->new('https://api.robinhood.com/options/chains/')
1190             ->query(
1191             { ( grep {
1192 3         422 ref $_ eq 'Finance::Robinhood::Equity::Instrument'
1193             } @filter
1194             )
1195 2         12 ? (equity_instrument_ids => [map { $_->id } @filter])
1196             : (grep {
1197 1         6 ref $_ eq 'Finance::Robinhood::Options::Instrument'
1198             } @filter
1199 6 100       50 ) ? (ids => [map { $_->chain_id } @filter]) : ()
  1 100       5  
1200             }
1201             ),
1202             _class => 'Finance::Robinhood::Options::Chain'
1203             );
1204             }
1205              
1206             sub _test_options_chains {
1207 1     1   3059 my $rh = t::Utility::rh_instance(0);
1208 1         11 my $chains = $rh->options_chains;
1209 1         216 isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
1210 1         278 isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
1211              
1212             # Get by equity instrument
1213 1         430 $chains = $rh->options_chains($rh->search('MSFT')->equity_instruments);
1214 1         1299 isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
1215 1         451 isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
1216 1         460 is($chains->current->symbol, 'MSFT');
1217              
1218             # Get by options instrument
1219 1         644 my ($instrument) = $rh->search('MSFT')->equity_instruments;
1220 1         68 my $options =
1221             $rh->options_instruments(chain_id => $instrument->tradable_chain_id,
1222             tradability => 'tradable');
1223 1         307 $chains = $rh->options_chains($options->next);
1224 1         191 isa_ok($chains, 'Finance::Robinhood::Utilities::Iterator');
1225 1         427 isa_ok($chains->next, 'Finance::Robinhood::Options::Chain');
1226 1         434 is($chains->current->symbol, 'MSFT');
1227             }
1228              
1229             =head2 C
1230              
1231             my $options = $rh->options_instruments();
1232              
1233             Returns an iterator containing Finance::Robinhood::Options::Instrument objects.
1234              
1235             my $options = $rh->options_instruments( state => 'active', type => 'put' );
1236              
1237             You can filter the results several ways. All of them are optional.
1238              
1239             =over
1240              
1241             =item C - C, C, or C
1242              
1243             =item C - C or C
1244              
1245             =item C - comma separated list of days; format is YYYY-M-DD
1246              
1247             =back
1248              
1249             =cut
1250              
1251 1     1 1 10 sub options_instruments ($s, %filters) {
  1         3  
  1         6  
  1         3  
1252              
1253             #$filters{chain_id} = $filters{chain}->chain_id if $filters{chain};
1254             # - ids - comma separated list of options ids (optional)
1255             # - cursor - paginated list position (optional)
1256             # - tradability - 'tradable' or 'untradable' (optional)
1257             # - state - 'active', 'inactive', or 'expired' (optional)
1258             # - type - 'put' or 'call' (optional)
1259             # - expiration_dates - comma separated list of days (optional; YYYY-MM-DD)
1260             # - chain_id - related options chain id (optional; UUID)
1261 1         7 Finance::Robinhood::Utilities::Iterator->new(
1262             _rh => $s,
1263             _next_page =>
1264             Mojo::URL->new('https://api.robinhood.com/options/instruments/')
1265             ->query(\%filters),
1266             _class => 'Finance::Robinhood::Options::Instrument'
1267             );
1268             }
1269              
1270             sub _test_options_instruments {
1271 1     1   3949 my $rh = t::Utility::rh_instance(1);
1272 0         0 my $options =
1273             $rh->options_instruments(
1274             chain_id =>
1275             $rh->equity_instrument_by_symbol('MSFT')->tradable_chain_id,
1276             tradability => 'tradable'
1277             );
1278 0         0 isa_ok($options, 'Finance::Robinhood::Utilities::Iterator');
1279 0         0 isa_ok($options->next, 'Finance::Robinhood::Options::Instrument');
1280 0         0 is($options->current->chain_symbol, 'MSFT');
1281             }
1282              
1283             =head1 UNSORTED
1284              
1285              
1286             =head2 C
1287              
1288             my $me = $rh->user();
1289              
1290             Returns a Finance::Robinhood::User object. You need to be logged in for this to
1291             work.
1292              
1293             =cut
1294              
1295 0     0 1 0 sub user ( $s ) {
  0         0  
  0         0  
1296 0         0 my $res = $s->_get('https://api.robinhood.com/user/');
1297 0 0       0 require Finance::Robinhood::User if $res->is_success;
1298             return $res->is_success
1299 0 0       0 ? Finance::Robinhood::User->new(_rh => $s, %{$res->json})
  0 0       0  
1300             : Finance::Robinhood::Error->new(
1301             $res->is_server_error ? (details => $res->message) : $res->json);
1302             }
1303              
1304             sub _test_user {
1305 1     1   2958 my $rh = t::Utility::rh_instance(1);
1306 0         0 my $me = $rh->user();
1307 0         0 isa_ok($me, 'Finance::Robinhood::User');
1308             }
1309              
1310             =head2 C
1311              
1312             my $acats = $rh->acats_transfers();
1313              
1314             An iterator containing Finance::Robinhood::ACATS::Transfer objects is returned.
1315              
1316             You need to be logged in for this to work.
1317              
1318             =cut
1319              
1320 0     0 1 0 sub acats_transfers ($s) {
  0         0  
  0         0  
1321 0         0 Finance::Robinhood::Utilities::Iterator->new(
1322             _rh => $s,
1323             _next_page => 'https://api.robinhood.com/acats/',
1324             _class => 'Finance::Robinhood::ACATS::Transfer'
1325             );
1326             }
1327              
1328             sub _test_acats_transfers {
1329 1     1   7506 my $transfers = t::Utility::rh_instance(1)->acats_transfers;
1330 0         0 isa_ok($transfers, 'Finance::Robinhood::Utilities::Iterator');
1331 0 0       0 skip_all('No ACATS transfers found') if !$transfers->has_next;
1332 0         0 isa_ok($transfers->current, 'Finance::Robinhood::ACATS::Transfer');
1333             }
1334              
1335             =head2 C
1336              
1337             my $positions = $rh->equity_positions( );
1338              
1339             Returns the related paginated list object filled with
1340             Finance::Robinhood::Equity::Position objects.
1341              
1342             You must be logged in.
1343              
1344             my $positions = $rh->equity_positions( nonzero => 1 );
1345              
1346             You can filter and modify the results. All options are optional.
1347              
1348             =over
1349              
1350             =item C - true or false. Default is false
1351              
1352             =item C - list of equity instruments
1353              
1354             =back
1355              
1356             =cut
1357              
1358 0     0 1 0 sub equity_positions ($s, %filters) {
  0         0  
  0         0  
  0         0  
1359             $filters{nonzero} = !!$filters{nonzero} ? 'true' : 'false'
1360 0 0       0 if defined $filters{nonzero};
    0          
1361 0         0 Finance::Robinhood::Utilities::Iterator->new(
1362             _rh => $s,
1363             _next_page => Mojo::URL->new('https://api.robinhood.com/positions/')
1364             ->query(\%filters),
1365             _class => 'Finance::Robinhood::Equity::Position'
1366             );
1367             }
1368              
1369             sub _test_equity_positions {
1370 1     1   1893 my $positions = t::Utility::rh_instance(1)->equity_positions;
1371 0         0 isa_ok($positions, 'Finance::Robinhood::Utilities::Iterator');
1372 0         0 isa_ok($positions->current, 'Finance::Robinhood::Equity::Position');
1373             }
1374              
1375             =head2 C
1376              
1377             my $earnings = $rh->equity_earnings( symbol => 'MSFT' );
1378              
1379             Returns the related paginated list object filled with
1380             Finance::Robinhood::Equity::Earnings objects by ticker symbol.
1381              
1382             my $earnings = $rh->equity_earnings( instrument => $rh->equity_instrument_by_symbol('MSFT') );
1383              
1384             Returns the related paginated list object filled with
1385             Finance::Robinhood::Equity::Earnings objects by instrument object/url.
1386              
1387             my $earnings = $rh->equity_earnings( range=> 7 );
1388              
1389             Returns a paginated list object filled with
1390             Finance::Robinhood::Equity::Earnings objects for all expected earnings report
1391             over the next C days where C is between C<-21...-1, 1...21>. Negative
1392             values are days into the past. Positive are days into the future.
1393              
1394             You must be logged in for any of these to work.
1395              
1396             =cut
1397              
1398 0     0 1 0 sub equity_earnings ($s, %filters) {
  0         0  
  0         0  
  0         0  
1399             $filters{range} = $filters{range} . 'day'
1400 0 0 0     0 if defined $filters{range} && $filters{range} =~ m[^\-?\d+$];
1401 0         0 Finance::Robinhood::Utilities::Iterator->new(
1402             _rh => $s,
1403             _next_page =>
1404             Mojo::URL->new('https://api.robinhood.com/marketdata/earnings/')
1405             ->query(\%filters),
1406             _class => 'Finance::Robinhood::Equity::Earnings'
1407             );
1408             }
1409              
1410             sub _test_equity_earnings {
1411 1     1   1553 my $by_instrument
1412             = t::Utility::rh_instance(1)
1413             ->equity_earnings(instrument =>
1414             t::Utility::rh_instance(1)->equity_instrument_by_symbol('MSFT'));
1415 0         0 isa_ok($by_instrument, 'Finance::Robinhood::Utilities::Iterator');
1416 0         0 isa_ok($by_instrument->current, 'Finance::Robinhood::Equity::Earnings');
1417 0         0 is($by_instrument->current->symbol,
1418             'MSFT', 'correct symbol (by instrument)');
1419             #
1420 0         0 my $by_symbol
1421             = t::Utility::rh_instance(1)->equity_earnings(symbol => 'MSFT');
1422 0         0 isa_ok($by_symbol, 'Finance::Robinhood::Utilities::Iterator');
1423 0         0 isa_ok($by_symbol->current, 'Finance::Robinhood::Equity::Earnings');
1424 0         0 is($by_symbol->current->symbol, 'MSFT', 'correct symbol (by symbol)');
1425              
1426             # Positive range
1427 0         0 my $p_range = t::Utility::rh_instance(1)->equity_earnings(range => 7);
1428 0         0 isa_ok($p_range, 'Finance::Robinhood::Utilities::Iterator');
1429 0         0 isa_ok($p_range->current, 'Finance::Robinhood::Equity::Earnings');
1430              
1431             # Negative range
1432 0         0 my $n_range = t::Utility::rh_instance(1)->equity_earnings(range => -7);
1433 0         0 isa_ok($n_range, 'Finance::Robinhood::Utilities::Iterator');
1434 0         0 isa_ok($n_range->current, 'Finance::Robinhood::Equity::Earnings');
1435             }
1436              
1437             =head1 FOREX METHODS
1438              
1439             Depending on your jurisdiction, your account may have access to Robinhood
1440             Crypto. See https://crypto.robinhood.com/ for more.
1441              
1442              
1443             =head2 C
1444              
1445             my $halts = $rh->forex_accounts;
1446              
1447             Returns an iterator full of Finance::Robinhood::Forex::Account objects.
1448              
1449             You need to be logged in and have access to Robinhood Crypto for this to work.
1450              
1451             =cut
1452              
1453 0     0 1 0 sub forex_accounts( $s ) {
  0         0  
  0         0  
1454 0         0 Finance::Robinhood::Utilities::Iterator->new(
1455             _rh => $s,
1456             _next_page =>
1457             Mojo::URL->new('https://nummus.robinhood.com/accounts/'),
1458             _class => 'Finance::Robinhood::Forex::Account'
1459             );
1460             }
1461              
1462             sub _test_forex_accounts {
1463 1     1   1886 my $halts = t::Utility::rh_instance(1)->forex_accounts;
1464 0         0 isa_ok($halts, 'Finance::Robinhood::Utilities::Iterator');
1465 0         0 isa_ok($halts->current, 'Finance::Robinhood::Forex::Account');
1466             }
1467              
1468             =head2 C
1469              
1470             my $account = $rh->forex_account_by_id($id);
1471              
1472             Returns a Finance::Robinhood::Forex::Account object. You need to be logged in
1473             for this to work.
1474              
1475             =cut
1476              
1477 0     0 1 0 sub forex_account_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1478 0         0 my $res = $s->_get('https://nummus.robinhood.com/accounts/' . $id . '/');
1479 0 0       0 require Finance::Robinhood::Forex::Account if $res->is_success;
1480             return $res->is_success
1481 0 0       0 ? Finance::Robinhood::Forex::Account->new(_rh => $s, %{$res->json})
  0 0       0  
1482             : Finance::Robinhood::Error->new(
1483             $res->is_server_error ? (details => $res->message) : $res->json);
1484             }
1485              
1486             sub _test_forex_account_by_id {
1487 1     1   2229 my $rh = t::Utility::rh_instance(1);
1488 0         0 my $acct = $rh->forex_account_by_id($rh->forex_accounts->current->id);
1489 0         0 isa_ok($acct, 'Finance::Robinhood::Forex::Account');
1490             }
1491              
1492             =head2 C
1493              
1494             my $halts = $rh->forex_halts;
1495             # or
1496             $halts = $rh->forex_halts( active => 1 );
1497              
1498             Returns an iterator full of Finance::Robinhood::Forex::Halt objects.
1499              
1500             If you pass a true value to a key named C, only active halts will be
1501             returned.
1502              
1503             You need to be logged in and have access to Robinhood Crypto for this to work.
1504              
1505             =cut
1506              
1507 0     0 1 0 sub forex_halts ($s, %filters) {
  0         0  
  0         0  
  0         0  
1508             $filters{active} = $filters{active} ? 'true' : 'false'
1509 0 0       0 if defined $filters{active};
    0          
1510 0         0 Finance::Robinhood::Utilities::Iterator->new(
1511             _rh => $s,
1512             _next_page => Mojo::URL->new('https://nummus.robinhood.com/halts/')
1513             ->query(\%filters),
1514             _class => 'Finance::Robinhood::Forex::Halt'
1515             );
1516             }
1517              
1518             sub _test_forex_halts {
1519 1     1   1887 my $halts = t::Utility::rh_instance(1)->forex_halts;
1520 0         0 isa_ok($halts, 'Finance::Robinhood::Utilities::Iterator');
1521 0         0 isa_ok($halts->current, 'Finance::Robinhood::Forex::Halt');
1522             #
1523 0         0 is( scalar $halts->all
1524             > scalar t::Utility::rh_instance(1)->forex_halts(active => 1)
1525             ->all,
1526             1,
1527             'active => 1 works'
1528             );
1529             }
1530              
1531             =head2 C
1532              
1533             my $currecies = $rh->forex_currencies();
1534              
1535             An iterator containing Finance::Robinhood::Forex::Currency objects is returned.
1536             You need to be logged in for this to work.
1537              
1538             =cut
1539              
1540 0     0 1 0 sub forex_currencies ($s) {
  0         0  
  0         0  
1541 0         0 Finance::Robinhood::Utilities::Iterator->new(
1542             _rh => $s,
1543             _next_page => 'https://nummus.robinhood.com/currencies/',
1544             _class => 'Finance::Robinhood::Forex::Currency'
1545             );
1546             }
1547              
1548             sub _test_forex_currencies {
1549 1     1   1875 my $rh = t::Utility::rh_instance(1);
1550 0         0 my $currencies = $rh->forex_currencies;
1551 0         0 isa_ok($currencies, 'Finance::Robinhood::Utilities::Iterator');
1552 0         0 isa_ok($currencies->current, 'Finance::Robinhood::Forex::Currency');
1553             }
1554              
1555             =head2 C
1556              
1557             my $currency = $rh->forex_currency_by_id($id);
1558              
1559             Returns a Finance::Robinhood::Forex::Currency object. You need to be logged in
1560             for this to work.
1561              
1562             =cut
1563              
1564 0     0 1 0 sub forex_currency_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1565 0         0 my $res
1566             = $s->_get('https://nummus.robinhood.com/currencies/' . $id . '/');
1567 0 0       0 require Finance::Robinhood::Forex::Currency if $res->is_success;
1568             return $res->is_success
1569 0 0       0 ? Finance::Robinhood::Forex::Currency->new(_rh => $s, %{$res->json})
  0 0       0  
1570             : Finance::Robinhood::Error->new(
1571             $res->is_server_error ? (details => $res->message) : $res->json);
1572             }
1573              
1574             sub _test_forex_currency_by_id {
1575 1     1   1915 my $rh = t::Utility::rh_instance(1);
1576 0         0 my $usd
1577             = $rh->forex_currency_by_id('1072fc76-1862-41ab-82c2-485837590762');
1578 0         0 isa_ok($usd, 'Finance::Robinhood::Forex::Currency');
1579             }
1580              
1581             =head2 C
1582              
1583             my $pairs = $rh->forex_pairs();
1584              
1585             An iterator containing Finance::Robinhood::Forex::Pair objects is returned. You
1586             need to be logged in for this to work.
1587              
1588             =cut
1589              
1590 0     0 1 0 sub forex_pairs ($s) {
  0         0  
  0         0  
1591 0         0 Finance::Robinhood::Utilities::Iterator->new(
1592             _rh => $s,
1593             _next_page => 'https://nummus.robinhood.com/currency_pairs/',
1594             _class => 'Finance::Robinhood::Forex::Pair'
1595             );
1596             }
1597              
1598             sub _test_forex_pairs {
1599 1     1   1899 my $rh = t::Utility::rh_instance(1);
1600 0         0 my $watchlists = $rh->forex_pairs;
1601 0         0 isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
1602 0         0 isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Pair');
1603             }
1604              
1605             =head2 C
1606              
1607             my $watchlist = $rh->forex_pair_by_id($id);
1608              
1609             Returns a Finance::Robinhood::Forex::Pair object. You need to be logged in for
1610             this to work.
1611              
1612             =cut
1613              
1614 0     0 1 0 sub forex_pair_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1615 0         0 my $res = $s->_get(
1616             'https://nummus.robinhood.com/currency_pairs/' . $id . '/');
1617 0 0       0 require Finance::Robinhood::Forex::Pair if $res->is_success;
1618             return $res->is_success
1619 0 0       0 ? Finance::Robinhood::Forex::Pair->new(_rh => $s, %{$res->json})
  0 0       0  
1620             : Finance::Robinhood::Error->new(
1621             $res->is_server_error ? (details => $res->message) : $res->json);
1622             }
1623              
1624             sub _test_forex_pair_by_id {
1625 1     1   1894 my $rh = t::Utility::rh_instance(1);
1626 0         0 my $btc_usd
1627             = $rh->forex_pair_by_id('3d961844-d360-45fc-989b-f6fca761d511')
1628             ; # BTC-USD
1629 0         0 isa_ok($btc_usd, 'Finance::Robinhood::Forex::Pair');
1630             }
1631              
1632             =head2 C
1633              
1634             my $btc = $rh->forex_pair_by_symbol('BTCUSD');
1635              
1636             Returns a Finance::Robinhood::Forex::Pair object. You need to be logged in for
1637             this to work.
1638              
1639             =cut
1640              
1641 0     0 1 0 sub forex_pair_by_symbol ($s, $id) {
  0         0  
  0         0  
  0         0  
1642 0         0 my $res = $s->_get(
1643             'https://nummus.robinhood.com/currency_pairs/?symbols=' . $id);
1644 0 0       0 require Finance::Robinhood::Forex::Pair if $res->is_success;
1645             return $res->is_success
1646 0 0       0 ? Finance::Robinhood::Forex::Pair->new(_rh => $s, %{$res->json})
  0 0       0  
1647             : Finance::Robinhood::Error->new(
1648             $res->is_server_error ? (details => $res->message) : $res->json);
1649             }
1650              
1651             sub _test_forex_pair_by_symbol {
1652 1     1   1867 my $rh = t::Utility::rh_instance(1);
1653 0         0 my $btc_usd = $rh->forex_pair_by_symbol('BTCUSD'); # BTC-USD
1654 0         0 isa_ok($btc_usd, 'Finance::Robinhood::Forex::Pair');
1655             }
1656              
1657             =head2 C
1658              
1659             my $watchlists = $rh->forex_watchlists();
1660              
1661             An iterator containing Finance::Robinhood::Forex::Watchlist objects is
1662             returned. You need to be logged in for this to work.
1663              
1664             =cut
1665              
1666 0     0 1 0 sub forex_watchlists ($s) {
  0         0  
  0         0  
1667 0         0 Finance::Robinhood::Utilities::Iterator->new(
1668             _rh => $s,
1669             _next_page => 'https://nummus.robinhood.com/watchlists/',
1670             _class => 'Finance::Robinhood::Forex::Watchlist'
1671             );
1672             }
1673              
1674             sub _test_forex_watchlists {
1675 1     1   1907 my $rh = t::Utility::rh_instance(1);
1676 0         0 my $watchlists = $rh->forex_watchlists;
1677 0         0 isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
1678 0         0 isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Watchlist');
1679             }
1680              
1681             =head2 C
1682              
1683             my $watchlist = $rh->forex_watchlist_by_id($id);
1684              
1685             Returns a Finance::Robinhood::Forex::Watchlist object. You need to be logged in
1686             for this to work.
1687              
1688             =cut
1689              
1690 0     0 1 0 sub forex_watchlist_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1691 0         0 my $res
1692             = $s->_get('https://nummus.robinhood.com/watchlists/' . $id . '/');
1693 0 0       0 require Finance::Robinhood::Forex::Watchlist if $res->is_success;
1694             return $res->is_success
1695 0 0       0 ? Finance::Robinhood::Forex::Watchlist->new(_rh => $s, %{$res->json})
  0 0       0  
1696             : Finance::Robinhood::Error->new(
1697             $res->is_server_error ? (details => $res->message) : $res->json);
1698             }
1699              
1700             sub _test_forex_watchlist_by_id {
1701 1     1   1857 my $rh = t::Utility::rh_instance(1);
1702 0         0 my $watchlist
1703             = $rh->forex_watchlist_by_id($rh->forex_watchlists->current->id);
1704 0         0 isa_ok($watchlist, 'Finance::Robinhood::Forex::Watchlist');
1705             }
1706              
1707             =head2 C
1708              
1709             my $activations = $rh->forex_activations();
1710              
1711             An iterator containing Finance::Robinhood::Forex::Activation objects is
1712             returned. You need to be logged in for this to work.
1713              
1714             =cut
1715              
1716 0     0 1 0 sub forex_activations ($s) {
  0         0  
  0         0  
1717 0         0 Finance::Robinhood::Utilities::Iterator->new(
1718             _rh => $s,
1719             _next_page => 'https://nummus.robinhood.com/activations/',
1720             _class => 'Finance::Robinhood::Forex::Activation'
1721             );
1722             }
1723              
1724             sub _test_forex_activations {
1725 1     1   2219 my $rh = t::Utility::rh_instance(1);
1726 0         0 my $watchlists = $rh->forex_activations;
1727 0         0 isa_ok($watchlists, 'Finance::Robinhood::Utilities::Iterator');
1728 0         0 isa_ok($watchlists->current, 'Finance::Robinhood::Forex::Activation');
1729             }
1730              
1731             =head2 C
1732              
1733             my $activation = $rh->forex_activation_by_id($id);
1734              
1735             Returns a Finance::Robinhood::Forex::Activation object. You need to be logged
1736             in for this to work.
1737              
1738             =cut
1739              
1740 0     0 1 0 sub forex_activation_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1741 0         0 my $res
1742             = $s->_get('https://nummus.robinhood.com/activations/' . $id . '/');
1743 0 0       0 require Finance::Robinhood::Forex::Activation if $res->is_success;
1744             return $res->is_success
1745 0 0       0 ? Finance::Robinhood::Forex::Activation->new(_rh => $s, %{$res->json})
  0 0       0  
1746             : Finance::Robinhood::Error->new(
1747             $res->is_server_error ? (details => $res->message) : $res->json);
1748             }
1749              
1750             sub _test_forex_activation_by_id {
1751 1     1   1899 my $rh = t::Utility::rh_instance(1);
1752 0         0 my $activation = $rh->forex_activations->current;
1753 0         0 my $forex = $rh->forex_activation_by_id($activation->id); # Cheat
1754 0         0 isa_ok($forex, 'Finance::Robinhood::Forex::Activation');
1755             }
1756              
1757             =head2 C
1758              
1759             my $portfolios = $rh->forex_portfolios();
1760              
1761             An iterator containing Finance::Robinhood::Forex::Portfolio objects is
1762             returned. You need to be logged in for this to work.
1763              
1764             =cut
1765              
1766 0     0 1 0 sub forex_portfolios ($s) {
  0         0  
  0         0  
1767 0         0 Finance::Robinhood::Utilities::Iterator->new(
1768             _rh => $s,
1769             _next_page => 'https://nummus.robinhood.com/portfolios/',
1770             _class => 'Finance::Robinhood::Forex::Portfolio'
1771             );
1772             }
1773              
1774             sub _test_forex_portfolios {
1775 1     1   1919 my $rh = t::Utility::rh_instance(1);
1776 0         0 my $portfolios = $rh->forex_portfolios;
1777 0         0 isa_ok($portfolios, 'Finance::Robinhood::Utilities::Iterator');
1778 0         0 isa_ok($portfolios->current, 'Finance::Robinhood::Forex::Portfolio');
1779             }
1780              
1781             =head2 C
1782              
1783             my $portfolio = $rh->forex_portfolio_by_id($id);
1784              
1785             Returns a Finance::Robinhood::Forex::Portfolio object. You need to be logged in
1786             for this to work.
1787              
1788             =cut
1789              
1790 0     0 1 0 sub forex_portfolio_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1791 0         0 my $res
1792             = $s->_get('https://nummus.robinhood.com/portfolios/' . $id . '/');
1793 0 0       0 require Finance::Robinhood::Forex::Portfolio if $res->is_success;
1794             return $res->is_success
1795 0 0       0 ? Finance::Robinhood::Forex::Portfolio->new(_rh => $s, %{$res->json})
  0 0       0  
1796             : Finance::Robinhood::Error->new(
1797             $res->is_server_error ? (details => $res->message) : $res->json);
1798             }
1799              
1800             sub _test_forex_portfolio_by_id {
1801 1     1   1857 my $rh = t::Utility::rh_instance(1);
1802 0         0 my $portfolio = $rh->forex_portfolios->current;
1803 0         0 my $forex = $rh->forex_portfolio_by_id($portfolio->id); # Cheat
1804 0         0 isa_ok($forex, 'Finance::Robinhood::Forex::Portfolio');
1805             }
1806              
1807             =head2 C
1808              
1809             my $activation = $rh->forex_activation_request( type => 'new_account' );
1810              
1811             Submits an application to activate a new forex account. If successful, a new
1812             Fiance::Robinhood::Forex::Activation object is returned. You need to be logged
1813             in for this to work.
1814              
1815             The following options are accepted:
1816              
1817             =over
1818              
1819             =item C
1820              
1821             This is required and must be one of the following:
1822              
1823             =over
1824              
1825             =item C
1826              
1827             =item C
1828              
1829             =back
1830              
1831             =item C
1832              
1833             This is an optional boolean value.
1834              
1835             =back
1836              
1837             =cut
1838              
1839 0     0 1 0 sub forex_activation_request ($s, %filters) {
  0         0  
  0         0  
  0         0  
1840             $filters{type} = $filters{type} ? 'true' : 'false'
1841 0 0       0 if defined $filters{type};
    0          
1842 0         0 my $res = $s->_post('https://nummus.robinhood.com/activations/')
1843             ->query(\%filters);
1844 0 0       0 require Finance::Robinhood::Forex::Activation if $res->is_success;
1845             return $res->is_success
1846 0 0       0 ? Finance::Robinhood::Forex::Activation->new(_rh => $s, %{$res->json})
  0 0       0  
1847             : Finance::Robinhood::Error->new(
1848             $res->is_server_error ? (details => $res->message) : $res->json);
1849             }
1850              
1851             sub _test_forex_activation_request {
1852 1     1   1869 diag(
1853             'This is one of those methods that is almost impossible to test from this side.'
1854             );
1855 1         566 pass(
1856             'Rather not have a million activation attempts attached to my account'
1857             );
1858             }
1859              
1860             =head2 C
1861              
1862             my $orders = $rh->forex_orders( );
1863              
1864             An iterator containing Finance::Robinhood::Forex::Order objects is returned.
1865             You need to be logged in for this to work.
1866              
1867             =cut
1868              
1869 0     0 1 0 sub forex_orders ($s) {
  0         0  
  0         0  
1870 0         0 Finance::Robinhood::Utilities::Iterator->new(
1871             _rh => $s,
1872             _next_page => Mojo::URL->new('https://nummus.robinhood.com/orders/'),
1873             _class => 'Finance::Robinhood::Forex::Order'
1874             );
1875             }
1876              
1877             sub _test_forex_orders {
1878 1     1   1874 my $rh = t::Utility::rh_instance(1);
1879 0         0 my $orders = $rh->forex_orders;
1880 0         0 isa_ok($orders, 'Finance::Robinhood::Utilities::Iterator');
1881 0         0 isa_ok($orders->current, 'Finance::Robinhood::Forex::Order');
1882             }
1883              
1884             =head2 C
1885              
1886             my $order = $rh->forex_order_by_id($id);
1887              
1888             Returns a Finance::Robinhood::Forex::Order object. You need to be logged in for
1889             this to work.
1890              
1891             =cut
1892              
1893 0     0 1 0 sub forex_order_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1894 0         0 my $res = $s->_get('https://nummus.robinhood.com/orders/' . $id . '/');
1895 0 0       0 require Finance::Robinhood::Forex::Order if $res->is_success;
1896             return $res->is_success
1897 0 0       0 ? Finance::Robinhood::Forex::Order->new(_rh => $s, %{$res->json})
  0 0       0  
1898             : Finance::Robinhood::Error->new(
1899             $res->is_server_error ? (details => $res->message) : $res->json);
1900             }
1901              
1902             sub _test_forex_order_by_id {
1903 1     1   1903 my $rh = t::Utility::rh_instance(1);
1904 0         0 my $order = $rh->forex_orders->current;
1905 0         0 my $forex = $rh->forex_order_by_id($order->id); # Cheat
1906 0         0 isa_ok($forex, 'Finance::Robinhood::Forex::Order');
1907             }
1908              
1909             =head2 C
1910              
1911             my $holdings = $rh->forex_holdings( );
1912              
1913             Returns the related paginated list object filled with
1914             Finance::Robinhood::Forex::Holding objects.
1915              
1916             You must be logged in.
1917              
1918             my $holdings = $rh->forex_holdings( nonzero => 1 );
1919              
1920             You can filter and modify the results. All options are optional.
1921              
1922             =over
1923              
1924             =item C - true or false. Default is false.
1925              
1926             =back
1927              
1928             =cut
1929              
1930 0     0 1 0 sub forex_holdings ($s, %filters) {
  0         0  
  0         0  
  0         0  
1931             $filters{nonzero} = !!$filters{nonzero} ? 'true' : 'false'
1932 0 0       0 if defined $filters{nonzero};
    0          
1933 0         0 Finance::Robinhood::Utilities::Iterator->new(
1934             _rh => $s,
1935             _next_page => Mojo::URL->new('https://nummus.robinhood.com/holdings/')
1936             ->query(\%filters),
1937             _class => 'Finance::Robinhood::Forex::Holding'
1938             );
1939             }
1940              
1941             sub _test_forex_holdings {
1942 1     1   1879 my $positions = t::Utility::rh_instance(1)->forex_holdings;
1943 0         0 isa_ok($positions, 'Finance::Robinhood::Utilities::Iterator');
1944 0         0 isa_ok($positions->current, 'Finance::Robinhood::Forex::Holding');
1945             }
1946              
1947             =head2 C
1948              
1949             my $holding = $rh->forex_holding_by_id($id);
1950              
1951             Returns a Finance::Robinhood::Forex::Holding object. You need to be logged in
1952             for this to work.
1953              
1954             =cut
1955              
1956 0     0 1 0 sub forex_holding_by_id ($s, $id) {
  0         0  
  0         0  
  0         0  
1957 0         0 my $res = $s->_get('https://nummus.robinhood.com/holdings/' . $id . '/');
1958 0 0       0 require Finance::Robinhood::Forex::Holding if $res->is_success;
1959             return $res->is_success
1960 0 0       0 ? Finance::Robinhood::Forex::Holding->new(_rh => $s, %{$res->json})
  0 0       0  
1961             : Finance::Robinhood::Error->new(
1962             $res->is_server_error ? (details => $res->message) : $res->json);
1963             }
1964              
1965             sub _test_forex_holding_by_id {
1966 1     1   1908 my $rh = t::Utility::rh_instance(1);
1967 0           my $holding = $rh->forex_holding_by_id($rh->forex_holdings->current->id);
1968 0           isa_ok($holding, 'Finance::Robinhood::Forex::Holding');
1969             }
1970              
1971             =head1 LEGAL
1972              
1973             This is a simple wrapper around the API used in the official apps. The author
1974             provides no investment, legal, or tax advice and is not responsible for any
1975             damages incurred while using this software. This software is not affiliated
1976             with Robinhood Financial LLC in any way.
1977              
1978             For Robinhood's terms and disclosures, please see their website at
1979             https://robinhood.com/legal/
1980              
1981             =head1 LICENSE
1982              
1983             Copyright (C) Sanko Robinson.
1984              
1985             This library is free software; you can redistribute it and/or modify it under
1986             the terms found in the Artistic License 2. Other copyrights, terms, and
1987             conditions may apply to data transmitted through this module. Please refer to
1988             the L section.
1989              
1990             =head1 AUTHOR
1991              
1992             Sanko Robinson Esanko@cpan.orgE
1993              
1994             =cut
1995              
1996             1;