File Coverage

lib/Finance/Robinhood.pm
Criterion Covered Total %
statement 274 622 44.0
branch 25 180 13.8
condition 9 45 20.0
subroutine 80 119 67.2
pod 50 50 100.0
total 438 1016 43.1


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