| 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; |