File Coverage

lib/Finance/Alpaca.pm
Criterion Covered Total %
statement 296 334 88.6
branch 42 106 39.6
condition 1 2 50.0
subroutine 55 61 90.1
pod 32 33 96.9
total 426 536 79.4


line stmt bran cond sub pod time code
1             package Finance::Alpaca 0.9904 {
2 17     17   3355706 use strictures 2;
  17         24274  
  17         605  
3 17     17   11867 use Moo;
  17         167917  
  17         79  
4 17     17   22347 use feature 'signatures';
  17         30  
  17         2069  
5 17     17   98 no warnings 'experimental::signatures';
  17         26  
  17         484  
6 17     17   8462 use Mojo::UserAgent;
  17         6208344  
  17         189  
7 17     17   11334 use Types::Standard qw[ArrayRef Bool Dict Enum InstanceOf Maybe Num Str Int];
  17         1478974  
  17         337  
8 17     17   36040 use Types::UUID;
  17         229021  
  17         257  
9             #
10 17     17   7187 use lib '../../lib/';
  17         32  
  17         119  
11 17     17   9588 use Finance::Alpaca::DataStream;
  17         69  
  17         714  
12 17     17   7713 use Finance::Alpaca::Struct::Account qw[to_Account];
  17         55  
  17         165  
13 17     17   15822 use Finance::Alpaca::Struct::Activity qw[to_Activity Activity];
  17         58  
  17         171  
14 17     17   17807 use Finance::Alpaca::Struct::Asset qw[to_Asset Asset];
  17         56  
  17         177  
15 17     17   9898 use Finance::Alpaca::Struct::Bar qw[to_Bar Bar];
  17         33  
  17         117  
16 17     17   14015 use Finance::Alpaca::Struct::Calendar qw[to_Calendar Calendar];
  17         50  
  17         143  
17 17     17   16025 use Finance::Alpaca::Struct::Configuration qw[to_Configuration Configuration];
  17         56  
  17         166  
18 17     17   17036 use Finance::Alpaca::Struct::Clock qw[to_Clock];
  17         44  
  17         138  
19 17     17   13749 use Finance::Alpaca::Struct::Order qw[to_Order Order];
  17         55  
  17         245  
20 17     17   16800 use Finance::Alpaca::Struct::Position qw[to_Position Position];
  17         51  
  17         170  
21 17     17   9392 use Finance::Alpaca::Struct::Quote qw[to_Quote Quote];
  17         35  
  17         119  
22 17     17   6782 use Finance::Alpaca::Struct::Trade qw[to_Trade Trade];
  17         34  
  17         117  
23 17     17   14144 use Finance::Alpaca::Struct::TradeActivity qw[to_TradeActivity TradeActivity];
  17         55  
  17         181  
24 17     17   17257 use Finance::Alpaca::Struct::Watchlist qw[to_Watchlist Watchlist];
  17         56  
  17         167  
25 17     17   16564 use Finance::Alpaca::TradeStream;
  17         51  
  17         667  
26 17     17   472 use Finance::Alpaca::Types;
  17         34  
  17         145  
27             #
28             has ua => ( is => 'lazy', isa => InstanceOf ['Mojo::UserAgent'] );
29              
30 16     16   2100 sub _build_ua ($s) {
  16         37  
  16         25  
31 16         312 my $ua = Mojo::UserAgent->new;
32 16         178 $ua->transactor->name(
33             sprintf 'Finance::Alpaca %f (Perl %s)',
34             $Finance::Alpaca::VERSION, $^V
35             );
36 41         77 $ua->on(
37 41     41   96 start => sub ( $ua, $tx ) {
  41         22150  
  41         79  
38 41 50       360 $tx->req->headers->header( 'APCA-API-KEY-ID' => $s->keys->[0] ) if $s->has_keys;
39 41 50       2236 $tx->req->headers->header( 'APCA-API-SECRET-KEY' => $s->keys->[1] ) if $s->has_keys;
40             }
41 16         1243 );
42 16         457 return $ua;
43             }
44             has api_version => ( is => 'ro', isa => Enum [ 1, 2 ], required => 1, default => 2 );
45             has paper => ( is => 'rw', isa => Bool, required => 1, default => 0, coerce => 1 );
46              
47 33     33 0 939 sub endpoint ($s) {
  33         60  
  33         49  
48 33 50       570 $s->paper ? 'https://paper-api.alpaca.markets' : '';
49             }
50             has keys => ( is => 'rwp', isa => ArrayRef [ Str, 2 ], predicate => 1 );
51             #
52 1     1 1 3850 sub account ($s) {
  1         3  
  1         2  
53 1         24 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/account' );
54 1         433 $tx = $s->ua->start($tx);
55 1         512426 return to_Account( $tx->result->json );
56             }
57              
58 3     3 1 10325 sub clock ($s) {
  3         8  
  3         6  
59 3         61 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/clock' );
60 3         1255 $tx = $s->ua->start($tx);
61 3         1093013 return to_Clock( $tx->result->json );
62             }
63              
64 1     1 1 3850 sub calendar ( $s, %params ) {
  1         3  
  1         6  
  1         2  
65 1         2 my $params = '';
66             $params .= '?' . join '&', map {
67 1 50       10 $_ . '='
68 2 50       14 . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
69             } keys %params if keys %params;
70 1         25 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/calendar' . $params );
71 1         568 $tx = $s->ua->start($tx);
72 1         393360 return @{ ( ArrayRef [Calendar] )->assert_coerce( $tx->result->json ) };
  1         7  
73             }
74              
75 0     0 1 0 sub assets ( $s, %params ) {
  0         0  
  0         0  
  0         0  
76 0         0 my $params = '';
77             $params .= '?' . join '&', map {
78 0 0       0 $_ . '='
79 0 0       0 . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
80             } keys %params if keys %params;
81             return @{
82 0         0 ( ArrayRef [Asset] )->assert_coerce(
  0         0  
83             $s->ua->get( $s->endpoint . '/v2/assets' . $params )->result->json
84             )
85             };
86              
87             }
88              
89 2     2 1 19929 sub asset ( $s, $symbol_or_asset_id ) {
  2         6  
  2         4  
  2         4  
90 2         69 my $res = $s->ua->get( $s->endpoint . '/v2/assets/' . $symbol_or_asset_id )->result;
91 2 50       455309 return $res->is_error ? () : to_Asset( $res->json );
92             }
93              
94 2     2 1 9573 sub bars ( $s, %params ) {
  2         5  
  2         15  
  2         4  
95 2         5 my $symbol = delete $params{symbol};
96 2         6 my $params = '';
97             $params .= '?' . join '&', map {
98 2 50       14 $_ . '='
99             . (
100             ref $params{$_} eq 'Time::Moment'
101             ? $params{$_}->strftime('%Y-%m-%dT%H:%M:%S%Z')
102 7 50       28 : $params{$_}
103             )
104             } keys %params if keys %params;
105 2         54 my $res = $s->ua->get(
106             sprintf 'https://data.alpaca.markets/v%d/stocks/%s/bars%s',
107             $s->api_version, $symbol, $params
108             )->result;
109             return $res->is_error ? $res->json : (
110             ( next_page_token => $res->json->{next_page_token} ),
111 2 50       757009 map { delete $_->{symbol} => delete $_->{bars} }
  2         676483  
112             ( Dict [ bars => ArrayRef [Bar], symbol => Str, next_page_token => Maybe [Str] ] )
113             ->assert_coerce( $res->json )
114             );
115             }
116              
117 2     2 1 9775 sub quotes ( $s, %params ) {
  2         4  
  2         11  
  2         4  
118 2         4 my $symbol = delete $params{symbol};
119 2         6 my $params = '';
120             $params .= '?' . join '&', map {
121 2 50       13 $_ . '='
122 5 50       25 . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
123             } keys %params if keys %params;
124 2         50 my $res = $s->ua->get(
125             sprintf 'https://data.alpaca.markets/v%d/stocks/%s/quotes%s',
126             $s->api_version, $symbol, $params
127             )->result;
128             return $res->is_error ? $res->json : (
129             ( next_page_token => $res->json->{next_page_token} ),
130 2 50       903350 map { delete $_->{symbol} => delete $_->{quotes} } (
  2         868072  
131             Dict [ quotes => ArrayRef [Quote], symbol => Str, next_page_token => Maybe [Str] ]
132             )->assert_coerce( $res->json )
133             );
134             }
135              
136 2     2 1 10812 sub trades ( $s, %params ) {
  2         6  
  2         15  
  2         3  
137 2         6 my $symbol = delete $params{symbol};
138 2         8 for ( keys %params ) {
139 5 50       15 $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
140             }
141 2         61 my $res = $s->ua->get(
142             sprintf(
143             'https://data.alpaca.markets/v%d/stocks/%s/trades',
144             $s->api_version, $symbol
145             ) => form => {%params}
146             )->result;
147             return $res->is_error ? $res->json : (
148             ( next_page_token => $res->json->{next_page_token} ),
149 2 50       904682 map { delete $_->{symbol} => delete $_->{trades} } (
  2         867601  
150             Dict [ trades => ArrayRef [Trade], symbol => Str, next_page_token => Maybe [Str] ]
151             )->assert_coerce( $res->json )
152             );
153             }
154              
155 1     1 1 6280 sub trade_stream ( $s, $cb, %params ) {
  1         4  
  1         2  
  1         2  
  1         2  
156 1         8 my $stream = Finance::Alpaca::TradeStream->new( cb => $cb );
157 0         0 $stream->authorize( $s->ua, $s->keys, $s->paper )->catch(
158 0     0   0 sub ($err) {
  0         0  
159 0         0 $stream = ();
160 0         0 warn "WebSocket error: $err";
161             }
162 1         2068 )->wait;
163 1         1116 $stream;
164             }
165              
166 1     1 1 5793 sub data_stream ( $s, $cb, %params ) {
  1         2  
  1         2  
  1         2  
  1         2  
167             my $stream = Finance::Alpaca::DataStream->new(
168             cb => $cb,
169 1   50     13 source => delete $params{source} // 'iex' # iex or sip
170             );
171 0         0 $stream->authorize( $s->ua, $s->keys )->catch(
172 0     0   0 sub ($err) {
  0         0  
173 0         0 $stream = ();
174 0         0 warn "WebSocket error: $err";
175             }
176 1         2689 )->wait;
177 1         1161 $stream;
178             }
179              
180 2     2 1 11422 sub orders ( $s, %params ) {
  2         4  
  2         7  
  2         3  
181 2         9 for ( keys %params ) {
182 2 50       11 $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
183             }
184             return @{
185 2         3 ( ArrayRef [Order] )->assert_coerce(
  2         12  
186             $s->ua->get( $s->endpoint . '/v2/orders' => form => {%params} )->result->json
187             )
188             };
189             }
190              
191 3     3 1 103931 sub order_by_id ( $s, $order_id, $nested = 0 ) {
  3         7  
  3         7  
  3         5  
  3         5  
192 3 50       79 my $res
193             = $s->ua->get(
194             $s->endpoint . '/v2/orders/' . $order_id => form => ( $nested ? { nested => 1 } : () ) )
195             ->result;
196 3 50       288791 return $res->is_error ? () : to_Order( $res->json );
197             }
198              
199 1     1 1 2873 sub order_by_client_id ( $s, $order_id ) {
  1         2  
  1         2  
  1         1  
200 1         24 my $res
201             = $s->ua->get( $s->endpoint
202             . '/v2/orders:by_client_order_id' => form => { client_order_id => $order_id } )
203             ->result;
204 1 50       132242 return $res->is_error ? () : to_Order( $res->json );
205             }
206              
207 3     3 1 5416800 sub create_order ( $s, %params ) {
  3         8  
  3         28  
  3         6  
208             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
209 3 0       17 if defined $params{extended_hours};
    50          
210 3         96 my $res = $s->ua->post( $s->endpoint . '/v2/orders' => json => \%params )->result;
211 3 50       770597 return $res->is_error ? $res->json : to_Order( $res->json );
212             }
213              
214 1     1 1 2698 sub replace_order ( $s, $order_id, %params ) {
  1         2  
  1         1  
  1         3  
  1         2  
215             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
216 1 0       4 if defined $params{extended_hours};
    50          
217 1         24 my $res
218             = $s->ua->patch( $s->endpoint . '/v2/orders/' . $order_id => json => \%params )->result;
219 1 50       124391 return $res->is_error ? $res->json : to_Order( $res->json );
220             }
221              
222 0     0 1 0 sub cancel_orders ($s) {
  0         0  
  0         0  
223 0         0 my $res = $s->ua->delete( $s->endpoint . '/v2/orders' )->result;
224 0 0       0 return $res->is_error
225             ? $res->json
226             : ( ArrayRef [ Dict [ body => Order, id => Uuid, status => Int ] ] )
227             ->assert_coerce( $res->json );
228             }
229              
230 1     1 1 3143 sub cancel_order ( $s, $order_id ) {
  1         1  
  1         2  
  1         1  
231 1         22 my $res = $s->ua->delete( $s->endpoint . '/v2/orders/' . $order_id )->result;
232 1         99575 return !$res->is_error;
233             }
234              
235 1     1 1 3604 sub positions ($s) {
  1         2  
  1         2  
236             return
237 1         2 @{ ( ArrayRef [Position] )
  1         5  
238             ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/positions' )->result->json ) };
239             }
240              
241 2     2 1 393978 sub position ( $s, $symbol_or_asset_id ) {
  2         6  
  2         5  
  2         3  
242 2         64 my $res = $s->ua->get( $s->endpoint . '/v2/positions/' . $symbol_or_asset_id )->result;
243 2 50       185753 return $res->is_error ? () : to_Position( $res->json );
244             }
245              
246 0     0 1 0 sub close_all_positions ( $s, $cancel_orders = !1 ) {
  0         0  
  0         0  
  0         0  
247 0 0       0 my $res
248             = $s->ua->delete(
249             $s->endpoint . '/v2/positions' . ( $cancel_orders ? '?cancel_orders=true' : '' ) )
250             ->result;
251 0 0       0 return $res->is_error
252             ? $res->json
253             : ( ArrayRef [ Dict [ body => Order, id => Uuid, status => Int ] ] )
254             ->assert_coerce( $res->json );
255             }
256              
257 0     0 1 0 sub close_position ( $s, $symbol_or_asset_id, $qty = () ) {
  0         0  
  0         0  
  0         0  
  0         0  
258 0 0       0 my $res
259             = $s->ua->get(
260             $s->endpoint . '/v2/positions/' . $symbol_or_asset_id . ( $qty ? '?qty=' . $qty : '' ) )
261             ->result;
262 0 0       0 return $res->is_error ? () : to_Order( $res->json );
263             }
264              
265 1     1 1 3499 sub portfolio_history ( $s, %params ) {
  1         3  
  1         2  
  1         1  
266             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
267 1 0       5 if defined $params{extended_hours};
    50          
268             $params{date_end}
269             = ref $params{date_end} eq 'Time::Moment'
270             ? $params{date_end}->strftime('%F')
271             : $params{date_end}
272 1 0       3 if defined $params{date_end};
    50          
273 1         21 my $res = $s->ua->get( $s->endpoint . '/v2/account/portfolio/history' => json => \%params )
274             ->result;
275 1 50       417641 return $res->is_error ? $res->json : (
276             Dict [
277             base_value => Num,
278             equity => ArrayRef [Num],
279             profit_loss => ArrayRef [Num],
280             profit_loss_pct => ArrayRef [Num],
281             timeframe => Str,
282             timestamp => ArrayRef [Timestamp]
283             ]
284             )->assert_coerce( $res->json );
285             }
286              
287 1     1 1 3537 sub watchlists ($s) {
  1         3  
  1         3  
288             return
289 1         2 @{ ( ArrayRef [Watchlist] )
  1         7  
290             ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/watchlists' )->result->json ) };
291             }
292              
293 1     1 1 470322 sub create_watchlist ( $s, $name, @symbols ) {
  1         4  
  1         2  
  1         4  
  1         1  
294 1 50       37 my $res
295             = $s->ua->post( $s->endpoint
296             . '/v2/watchlists' => json =>
297             { name => $name, ( @symbols ? ( symbols => \@symbols ) : () ) } )->result;
298 1 50       111553 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
299             }
300              
301 1     1 1 2834 sub delete_watchlist ( $s, $watchlist_id ) {
  1         3  
  1         2  
  1         1  
302 1         23 my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
303 1 50       93454 return $res->is_error ? $res->json : 1;
304             }
305              
306 1     1 1 10664 sub watchlist ( $s, $watchlist_id ) {
  1         3  
  1         2  
  1         2  
307 1         19 my $res = $s->ua->get( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
308 1 50       92454 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
309             }
310              
311 2     2 1 8113 sub update_watchlist ( $s, $watchlist_id, %params ) {
  2         5  
  2         5  
  2         6  
  2         4  
312 2         51 my $res
313             = $s->ua->put( $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => {%params} )
314             ->result;
315 2 50       201834 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
316             }
317              
318 1     1 1 2989 sub add_to_watchlist ( $s, $watchlist_id, $symbol ) {
  1         2  
  1         3  
  1         2  
  1         1  
319 1         27 my $res
320             = $s->ua->post(
321             $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => { symbol => $symbol } )
322             ->result;
323 1 50       97145 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
324             }
325              
326 1     1 1 3727 sub remove_from_watchlist ( $s, $watchlist_id, $symbol ) {
  1         2  
  1         3  
  1         2  
  1         3  
327 1         25 my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id . '/' . $symbol )
328             ->result;
329 1 50       95273 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
330             }
331              
332 1     1 1 3695 sub configuration ($s) {
  1         3  
  1         2  
333 1         21 my $res = $s->ua->get( $s->endpoint . '/v2/account/configurations' )->result;
334 1 50       384914 return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
335             }
336              
337 1     1 1 12366 sub modify_configuration ( $s, %params ) {
  1         4  
  1         3  
  1         2  
338 1         37 my $res = $s->ua->patch( $s->endpoint . '/v2/account/configurations' => json => {%params} )
339             ->result;
340 1 50       97535 return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
341             }
342              
343 1     1 1 3334 sub activities ( $s, %params ) {
  1         3  
  1         3  
  1         2  
344 1 50       4 $params{activity_types} = join ',', @{ $params{activity_types} } if $params{activity_types};
  1         3  
345 1         2 my $params = '';
346             $params .= '?' . join '&', map {
347 1 50       6 $_ . '='
348             . (
349             ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string()
350 0         0 : ref $params{$_} eq 'ARRAY' ? @{ $params{$_} }
351 1 50       10 : $params{$_}
    50          
352             )
353             } keys %params if keys %params;
354 1 50       22 my $res = $s->ua->get(
355             sprintf $s->endpoint . '/v2/account/activities%s',
356             $params ? $params : ''
357             )->result;
358             return $res->is_error
359             ? $res->json
360 100 50       71159 : map { $_->{activity_type} eq 'FILL' ? to_TradeActivity($_) : to_Activity($_) }
361 1 50       392249 @{ $res->json };
  1         30  
362             }
363             }
364             1;
365             __END__