File Coverage

lib/Finance/Alpaca.pm
Criterion Covered Total %
statement 274 334 82.0
branch 42 106 39.6
condition 0 2 0.0
subroutine 52 61 85.2
pod 32 33 96.9
total 400 536 74.6


line stmt bran cond sub pod time code
1             package Finance::Alpaca 0.9906 {
2 17     17   3974255 use strictures 2;
  17         29207  
  17         752  
3 17     17   14157 use Moo;
  17         204154  
  17         89  
4 17     17   26728 use feature 'signatures';
  17         37  
  17         2486  
5 17     17   120 no warnings 'experimental::signatures';
  17         30  
  17         534  
6 17     17   10114 use Mojo::UserAgent;
  17         7452531  
  17         240  
7 17     17   12288 use Types::Standard qw[ArrayRef Bool Dict Enum InstanceOf Maybe Num Str Int];
  17         1784467  
  17         297  
8 17     17   42865 use Types::UUID;
  17         271596  
  17         261  
9             #
10 17     17   7794 use lib '../../lib/';
  17         38  
  17         141  
11 17     17   11009 use Finance::Alpaca::DataStream;
  17         72  
  17         798  
12 17     17   8178 use Finance::Alpaca::Struct::Account qw[to_Account];
  17         60  
  17         151  
13 17     17   15842 use Finance::Alpaca::Struct::Activity qw[to_Activity Activity];
  17         60  
  17         173  
14 17     17   18565 use Finance::Alpaca::Struct::Asset qw[to_Asset Asset];
  17         56  
  17         161  
15 17     17   10200 use Finance::Alpaca::Struct::Bar qw[to_Bar Bar];
  17         42  
  17         125  
16 17     17   15795 use Finance::Alpaca::Struct::Calendar qw[to_Calendar Calendar];
  17         54  
  17         138  
17 17     17   16805 use Finance::Alpaca::Struct::Configuration qw[to_Configuration Configuration];
  17         57  
  17         167  
18 17     17   17539 use Finance::Alpaca::Struct::Clock qw[to_Clock];
  17         55  
  17         187  
19 17     17   14799 use Finance::Alpaca::Struct::Order qw[to_Order Order];
  17         58  
  17         253  
20 17     17   19031 use Finance::Alpaca::Struct::Position qw[to_Position Position];
  17         55  
  17         150  
21 17     17   9816 use Finance::Alpaca::Struct::Quote qw[to_Quote Quote];
  17         37  
  17         140  
22 17     17   8321 use Finance::Alpaca::Struct::Trade qw[to_Trade Trade];
  17         37  
  17         132  
23 17     17   15282 use Finance::Alpaca::Struct::TradeActivity qw[to_TradeActivity TradeActivity];
  17         56  
  17         157  
24 17     17   16944 use Finance::Alpaca::Struct::Watchlist qw[to_Watchlist Watchlist];
  17         56  
  17         158  
25 17     17   16730 use Finance::Alpaca::TradeStream;
  17         53  
  17         617  
26 17     17   162 use Finance::Alpaca::Types;
  17         45  
  17         136  
27             #
28             has ua => ( is => 'lazy', isa => InstanceOf ['Mojo::UserAgent'] );
29              
30 16     16   1920 sub _build_ua ($s) {
  16         53  
  16         33  
31 16         327 my $ua = Mojo::UserAgent->new;
32 16         204 $ua->transactor->name(
33             sprintf 'Finance::Alpaca %f (Perl %s)',
34             $Finance::Alpaca::VERSION, $^V
35             );
36 35         77 $ua->on(
37 35     35   63 start => sub ( $ua, $tx ) {
  35         22148  
  35         72  
38 35 50       350 $tx->req->headers->header( 'APCA-API-KEY-ID' => $s->keys->[0] ) if $s->has_keys;
39 35 50       2108 $tx->req->headers->header( 'APCA-API-SECRET-KEY' => $s->keys->[1] ) if $s->has_keys;
40             }
41 16         1388 );
42 16         492 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 29     29 0 989 sub endpoint ($s) {
  29         64  
  29         55  
48 29 50       591 $s->paper ? 'https://paper-api.alpaca.markets' : '';
49             }
50             has keys => ( is => 'rwp', isa => ArrayRef [ Str, 2 ], predicate => 1 );
51             #
52 1     1 1 4028 sub account ($s) {
  1         3  
  1         2  
53 1         24 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/account' );
54 1         514 $tx = $s->ua->start($tx);
55 1         515134 return to_Account( $tx->result->json );
56             }
57              
58 3     3 1 12349 sub clock ($s) {
  3         9  
  3         6  
59 3         73 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/clock' );
60 3         1442 $tx = $s->ua->start($tx);
61 3         1142465 return to_Clock( $tx->result->json );
62             }
63              
64 1     1 1 4246 sub calendar ( $s, %params ) {
  1         3  
  1         5  
  1         2  
65 1         3 my $params = '';
66             $params .= '?' . join '&', map {
67 1 50       9 $_ . '='
68 2 50       15 . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
69             } keys %params if keys %params;
70 1         31 my $tx = $s->ua->build_tx( GET => $s->endpoint . '/v2/calendar' . $params );
71 1         596 $tx = $s->ua->start($tx);
72 1         384415 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 23681 sub asset ( $s, $symbol_or_asset_id ) {
  2         7  
  2         5  
  2         7  
90 2         75 my $res = $s->ua->get( $s->endpoint . '/v2/assets/' . $symbol_or_asset_id )->result;
91 2 50       465584 return $res->is_error ? () : to_Asset( $res->json );
92             }
93              
94 2     2 1 12519 sub bars ( $s, %params ) {
  2         6  
  2         17  
  2         4  
95 2         5 my $symbol = delete $params{symbol};
96 2         5 my $params = '';
97             $params .= '?' . join '&', map {
98 2 50       17 $_ . '='
99             . (
100             ref $params{$_} eq 'Time::Moment'
101             ? $params{$_}->strftime('%Y-%m-%dT%H:%M:%S%Z')
102 7 50       39 : $params{$_}
103             )
104             } keys %params if keys %params;
105 2         73 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       768980 map { delete $_->{symbol} => delete $_->{bars} }
  2         848890  
112             ( Dict [ bars => ArrayRef [Bar], symbol => Str, next_page_token => Maybe [Str] ] )
113             ->assert_coerce( $res->json )
114             );
115             }
116              
117 2     2 1 12614 sub quotes ( $s, %params ) {
  2         6  
  2         13  
  2         4  
118 2         7 my $symbol = delete $params{symbol};
119 2         6 my $params = '';
120             $params .= '?' . join '&', map {
121 2 50       17 $_ . '='
122 5 50       32 . ( ref $params{$_} eq 'Time::Moment' ? $params{$_}->to_string() : $params{$_} )
123             } keys %params if keys %params;
124 2         70 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       912726 map { delete $_->{symbol} => delete $_->{quotes} } (
  2         1076371  
131             Dict [ quotes => ArrayRef [Quote], symbol => Str, next_page_token => Maybe [Str] ]
132             )->assert_coerce( $res->json )
133             );
134             }
135              
136 2     2 1 12354 sub trades ( $s, %params ) {
  2         7  
  2         14  
  2         4  
137 2         6 my $symbol = delete $params{symbol};
138 2         10 for ( keys %params ) {
139 5 50       18 $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
140             }
141 2         54 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       906771 map { delete $_->{symbol} => delete $_->{trades} } (
  2         1059840  
150             Dict [ trades => ArrayRef [Trade], symbol => Str, next_page_token => Maybe [Str] ]
151             )->assert_coerce( $res->json )
152             );
153             }
154              
155 0     0 1 0 sub trade_stream ( $s, $cb, %params ) {
  0         0  
  0         0  
  0         0  
  0         0  
156 0         0 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 0         0 )->wait;
163 0         0 $stream;
164             }
165              
166 0     0 1 0 sub data_stream ( $s, $cb, %params ) {
  0         0  
  0         0  
  0         0  
  0         0  
167             my $stream = Finance::Alpaca::DataStream->new(
168             cb => $cb,
169 0   0     0 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 0         0 )->wait;
177 0         0 $stream;
178             }
179              
180 2     2 1 13074 sub orders ( $s, %params ) {
  2         6  
  2         8  
  2         4  
181 2         8 for ( keys %params ) {
182 2 50       12 $params{$_} = $params{$_}->to_string() if ref $params{$_} eq 'Time::Moment';
183             }
184             return @{
185 2         5 ( ArrayRef [Order] )->assert_coerce(
  2         13  
186             $s->ua->get( $s->endpoint . '/v2/orders' => form => {%params} )->result->json
187             )
188             };
189             }
190              
191 1     1 1 205562 sub order_by_id ( $s, $order_id, $nested = 0 ) {
  1         3  
  1         3  
  1         3  
  1         3  
192 1 50       27 my $res
193             = $s->ua->get(
194             $s->endpoint . '/v2/orders/' . $order_id => form => ( $nested ? { nested => 1 } : () ) )
195             ->result;
196 1 50       98813 return $res->is_error ? () : to_Order( $res->json );
197             }
198              
199 1     1 1 3412 sub order_by_client_id ( $s, $order_id ) {
  1         4  
  1         2  
  1         2  
200 1         29 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       98560 return $res->is_error ? () : to_Order( $res->json );
205             }
206              
207 2     2 1 534350 sub create_order ( $s, %params ) {
  2         20  
  2         17  
  2         4  
208             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
209 2 0       12 if defined $params{extended_hours};
    50          
210 2         56 my $res = $s->ua->post( $s->endpoint . '/v2/orders' => json => \%params )->result;
211 2 50       653303 return $res->is_error ? $res->json : to_Order( $res->json );
212             }
213              
214 1     1 1 3368 sub replace_order ( $s, $order_id, %params ) {
  1         3  
  1         4  
  1         4  
  1         2  
215             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
216 1 0       4 if defined $params{extended_hours};
    50          
217 1         31 my $res
218             = $s->ua->patch( $s->endpoint . '/v2/orders/' . $order_id => json => \%params )->result;
219 1 50       100121 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 0     0 1 0 sub cancel_order ( $s, $order_id ) {
  0         0  
  0         0  
  0         0  
231 0         0 my $res = $s->ua->delete( $s->endpoint . '/v2/orders/' . $order_id )->result;
232 0         0 return !$res->is_error;
233             }
234              
235 1     1 1 3987 sub positions ($s) {
  1         3  
  1         3  
236             return
237 1         2 @{ ( ArrayRef [Position] )
  1         6  
238             ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/positions' )->result->json ) };
239             }
240              
241 2     2 1 387947 sub position ( $s, $symbol_or_asset_id ) {
  2         6  
  2         7  
  2         2  
242 2         61 my $res = $s->ua->get( $s->endpoint . '/v2/positions/' . $symbol_or_asset_id )->result;
243 2 50       186272 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 4514 sub portfolio_history ( $s, %params ) {
  1         4  
  1         3  
  1         2  
266             $params{extended_hours} = ( !!$params{extended_hours} ) ? 'true' : 'false'
267 1 0       6 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       5 if defined $params{date_end};
    50          
273 1         27 my $res = $s->ua->get( $s->endpoint . '/v2/account/portfolio/history' => json => \%params )
274             ->result;
275 1 50       392572 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 4007 sub watchlists ($s) {
  1         3  
  1         2  
288             return
289 1         1 @{ ( ArrayRef [Watchlist] )
  1         7  
290             ->assert_coerce( $s->ua->get( $s->endpoint . '/v2/watchlists' )->result->json ) };
291             }
292              
293 1     1 1 393241 sub create_watchlist ( $s, $name, @symbols ) {
  1         3  
  1         4  
  1         3  
  1         3  
294 1 50       32 my $res
295             = $s->ua->post( $s->endpoint
296             . '/v2/watchlists' => json =>
297             { name => $name, ( @symbols ? ( symbols => \@symbols ) : () ) } )->result;
298 1 50       97805 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
299             }
300              
301 1     1 1 3597 sub delete_watchlist ( $s, $watchlist_id ) {
  1         3  
  1         3  
  1         2  
302 1         29 my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
303 1 50       93917 return $res->is_error ? $res->json : 1;
304             }
305              
306 1     1 1 12601 sub watchlist ( $s, $watchlist_id ) {
  1         3  
  1         2  
  1         2  
307 1         23 my $res = $s->ua->get( $s->endpoint . '/v2/watchlists/' . $watchlist_id )->result;
308 1 50       94326 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
309             }
310              
311 2     2 1 9836 sub update_watchlist ( $s, $watchlist_id, %params ) {
  2         6  
  2         4  
  2         8  
  2         4  
312 2         60 my $res
313             = $s->ua->put( $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => {%params} )
314             ->result;
315 2 50       199389 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
316             }
317              
318 1     1 1 3505 sub add_to_watchlist ( $s, $watchlist_id, $symbol ) {
  1         3  
  1         3  
  1         3  
  1         2  
319 1         28 my $res
320             = $s->ua->post(
321             $s->endpoint . '/v2/watchlists/' . $watchlist_id => json => { symbol => $symbol } )
322             ->result;
323 1 50       101166 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
324             }
325              
326 1     1 1 4738 sub remove_from_watchlist ( $s, $watchlist_id, $symbol ) {
  1         4  
  1         3  
  1         1  
  1         2  
327 1         29 my $res = $s->ua->delete( $s->endpoint . '/v2/watchlists/' . $watchlist_id . '/' . $symbol )
328             ->result;
329 1 50       93720 return $res->is_error ? ( $res->json ) : to_Watchlist( $res->json );
330             }
331              
332 1     1 1 4071 sub configuration ($s) {
  1         3  
  1         3  
333 1         25 my $res = $s->ua->get( $s->endpoint . '/v2/account/configurations' )->result;
334 1 50       362695 return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
335             }
336              
337 1     1 1 15803 sub modify_configuration ( $s, %params ) {
  1         3  
  1         4  
  1         2  
338 1         32 my $res = $s->ua->patch( $s->endpoint . '/v2/account/configurations' => json => {%params} )
339             ->result;
340 1 50       92958 return $res->is_error ? ( $res->json ) : to_Configuration( $res->json );
341             }
342              
343 1     1 1 4190 sub activities ( $s, %params ) {
  1         3  
  1         5  
  1         2  
344 1 50       5 $params{activity_types} = join ',', @{ $params{activity_types} } if $params{activity_types};
  1         4  
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       27 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       91932 : map { $_->{activity_type} eq 'FILL' ? to_TradeActivity($_) : to_Activity($_) }
361 1 50       405783 @{ $res->json };
  1         25  
362             }
363             }
364             1;
365             __END__