File Coverage

lib/Finance/Robinhood/Forex/OrderBuilder.pm
Criterion Covered Total %
statement 43 103 41.7
branch 1 10 10.0
condition 7 14 50.0
subroutine 18 26 69.2
pod 7 7 100.0
total 76 160 47.5


line stmt bran cond sub pod time code
1             package Finance::Robinhood::Forex::OrderBuilder;
2              
3             =encoding utf-8
4              
5             =for stopwords watchlist watchlists untradable urls
6              
7             =head1 NAME
8              
9             Finance::Robinhood::Forex::OrderBuilder - Provides a Sugary Builder-type
10             Interface for Generating a Forex Order
11              
12             =head1 SYNOPSIS
13              
14             use Finance::Robinhood;
15             my $rh = Finance::Robinhood->new;
16             my $btc_usd = $rh->forex_pair_by_id('3d961844-d360-45fc-989b-f6fca761d511');
17              
18             $btc_usd->buy(1)->submit;
19              
20             =head1 DESCRIPTION
21              
22             This is cotton candy for creating valid order structures.
23              
24             Without any additional method calls, this will create a simple market order
25             that looks like this:
26              
27             {
28             account => "XXXXXXXXXXXXXXXXXXXXXX",
29             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
30             price => "111.700000", # Automatically grabs ask or bid price quote
31             quantity => 4, # Actually the amount of crypto you requested
32             side => "buy", # Or sell
33             time_in_force => "ioc",
34             type => "market"
35             }
36              
37             You may chain together several methods to generate and submit advanced order
38             types such as stop limits that are held up to 90 days:
39              
40             $order->gtc->limit->submit;
41              
42             =cut
43              
44             our $VERSION = '0.92_001';
45 1     1   6 use Mojo::Base-base, -signatures;
  1         3  
  1         7  
46 1     1   637 use Finance::Robinhood::Forex::Order;
  1         3  
  1         6  
47              
48             sub _test__init {
49 1     1   2432 my $rh = t::Utility::rh_instance(1);
50 0         0 my $btc_usd = $rh->forex_pair_by_id('3d961844-d360-45fc-989b-f6fca761d511');
51 0         0 t::Utility::stash( 'BTC_USD', $btc_usd ); # Store it for later
52 0         0 isa_ok( $btc_usd->buy(3), __PACKAGE__ );
53 0         0 isa_ok( $btc_usd->sell(3), __PACKAGE__ );
54             }
55             #
56             has _rh => undef => weak => 1;
57              
58             =head1 METHODS
59              
60              
61             =head2 C
62              
63             Expects a Finance::Robinhood::Forex::Account object.
64              
65             =head2 C
66              
67             Expects a Finance::Robinhood::Forex::Pair object.
68              
69             =head2 C
70              
71             Expects a whole number of shares.
72              
73             =cut
74              
75             has _account => undef; # => weak => 1;
76             has _pair => undef; # => weak => 1;
77             has [ 'quantity', 'price' ];
78             #
79             # Type
80              
81             =head2 C
82              
83             $order->limit( 17.98 );
84              
85             Expects a price.
86              
87             Use this to create limit and stop limit orders.
88              
89             =head2 C
90              
91             $order->market( );
92              
93             Use this to create market and stop loss orders.
94              
95             =cut
96              
97 0     0 1 0 sub limit ( $s, $price ) {
  0         0  
  0         0  
  0         0  
98 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Limit')->limit($price);
99             }
100             {
101              
102             package Finance::Robinhood::Forex::OrderBuilder::Role::Limit;
103 1     1   217 use Mojo::Base-role, -signatures;
  1         3  
  1         5  
104             has limit => 0;
105             around _dump => sub ( $orig, $s, $test = 0 ) {
106             my %data = $orig->( $s, $test );
107             ( %data, price => $s->limit, type => 'limit' );
108             };
109             }
110              
111             sub _test_limit {
112 1   50 1   1882 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
113 0         0 my $order = t::Utility::stash('BTC_USD')->buy(3)->limit(3.40);
114 0         0 is(
115             { $order->_dump(1) },
116             {
117             account => "--private--",
118             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
119             price => 3.40,
120             quantity => 3,
121             ref_id => "00000000-0000-0000-0000-000000000000",
122             side => "buy",
123             time_in_force => "gtc",
124             type => "limit",
125             },
126             'dump is correct'
127             );
128             }
129              
130 0     0 1 0 sub market($s) {
  0         0  
  0         0  
131 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Market');
132             }
133             {
134              
135             package Finance::Robinhood::Forex::OrderBuilder::Role::Market;
136 1     1   706 use Mojo::Base-role, -signatures;
  1         2  
  1         6  
137             around _dump => sub ( $orig, $s, $test = 0 ) {
138             my %data = $orig->( $s, $test );
139             ( %data, type => 'market' );
140             };
141             }
142              
143             sub _test_market {
144 1   50 1   1818 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
145 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->market();
146 0         0 is(
147             { $order->_dump(1) },
148             {
149             account => "--private--",
150             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
151             price => '5.00',
152             quantity => 3,
153             ref_id => "00000000-0000-0000-0000-000000000000",
154             side => "sell",
155             time_in_force => "gtc",
156             type => "market",
157             },
158             'dump is correct'
159             );
160             }
161              
162             =begin internal
163              
164             =head2 C
165              
166             $order->buy( 3 );
167              
168             Use this to change the order side.
169              
170             =head2 C
171              
172             $order->sell( 4 );
173              
174             Use this to change the order side.
175              
176             =end internal
177              
178             =cut
179              
180             # Side
181 0     0 1 0 sub buy ( $s, $quantity = $s->quantity ) {
  0         0  
  0         0  
  0         0  
182 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Buy');
183 0         0 $s->quantity($quantity);
184             }
185             {
186              
187             package Finance::Robinhood::Forex::OrderBuilder::Role::Buy;
188 1     1   638 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
189             around _dump => sub ( $orig, $s, $test = 0 ) {
190             my %data = $orig->( $s, $test );
191             (
192             %data,
193             side => 'buy',
194             price => $test ? '5.00' : $s->price // $s->_pair->quote->bid_price
195             );
196             };
197             }
198              
199             sub _test_buy {
200 1   50 1   1880 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
201 0         0 my $order = t::Utility::stash('BTC_USD')->sell(32)->buy(3);
202 0         0 is(
203             { $order->_dump(1) },
204             {
205             account => "--private--",
206             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
207             price => '5.00',
208             quantity => 3,
209             ref_id => "00000000-0000-0000-0000-000000000000",
210             side => "buy",
211             time_in_force => "gtc",
212             type => "market",
213             },
214             'dump is correct'
215             );
216             }
217              
218 0     0 1 0 sub sell ( $s, $quantity = $s->quantity ) {
  0         0  
  0         0  
  0         0  
219 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Sell');
220 0         0 $s->quantity($quantity);
221             }
222             {
223              
224             package Finance::Robinhood::Forex::OrderBuilder::Role::Sell;
225 1     1   659 use Mojo::Base-role, -signatures;
  1         2  
  1         6  
226             around _dump => sub ( $orig, $s, $test = 0 ) {
227             my %data = $orig->( $s, $test );
228             (
229             %data,
230             side => 'sell',
231             price => $test ? '5.00' : $s->price // $s->_pair->quote->ask_price
232             );
233             };
234             }
235              
236             sub _test_sell {
237 1   50 1   1846 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
238 0         0 my $order = t::Utility::stash('BTC_USD')->buy(32)->sell(3);
239 0         0 is(
240             { $order->_dump(1) },
241             {
242             account => "--private--",
243             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
244             price => '5.00',
245             quantity => 3,
246             ref_id => "00000000-0000-0000-0000-000000000000",
247             side => "sell",
248             time_in_force => "gtc",
249             type => "market",
250             },
251             'dump is correct'
252             );
253             }
254              
255             # Time in force
256              
257             =head2 C
258              
259             $order->gtc( );
260              
261             Use this to change the order's time in force value to Good-Till-Cancelled
262             (actually 90 days from submission).
263              
264              
265             =head2 C
266              
267             $order->ioc( );
268              
269             Use this to change the order's time in force value to Immediate-Or-Cancel.
270              
271             This may require special permissions.
272              
273             =cut
274              
275 0     0 1 0 sub gtc($s) {
  0         0  
  0         0  
276 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::GTC');
277             }
278             {
279              
280             package Finance::Robinhood::Forex::OrderBuilder::Role::GTC;
281 1     1   632 use Mojo::Base-role, -signatures;
  1         17  
  1         7  
282             around _dump => sub ( $orig, $s, $test = 0 ) {
283             my %data = $orig->( $s, $test );
284             ( %data, time_in_force => 'gtc' );
285             };
286             }
287              
288             sub _test_gtc {
289 1   50 1   1874 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
290 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->gtc();
291 0         0 is(
292             { $order->_dump(1) },
293             {
294             account => "--private--",
295             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
296             price => '5.00',
297             quantity => 3,
298             ref_id => "00000000-0000-0000-0000-000000000000",
299             side => "sell",
300             time_in_force => "gtc",
301             type => "market",
302             },
303             'dump is correct'
304             );
305             }
306              
307 0     0 1 0 sub ioc($s) {
  0         0  
  0         0  
308 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::IOC');
309             }
310             {
311              
312             package Finance::Robinhood::Forex::OrderBuilder::Role::IOC;
313 1     1   671 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
314             around _dump => sub ( $orig, $s, $test = 0 ) {
315             my %data = $orig->( $s, $test );
316             ( %data, time_in_force => 'ioc' );
317             };
318             }
319              
320             sub _test_ioc {
321 1   50 1   1814 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
322 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->ioc();
323 0         0 is(
324             { $order->_dump(1) },
325             {
326             account => "--private--",
327             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
328             price => '5.00',
329             quantity => 3,
330             ref_id => "00000000-0000-0000-0000-000000000000",
331             side => "sell",
332             time_in_force => "ioc",
333             type => "market",
334             },
335             'dump is correct'
336             );
337             }
338              
339             # Do it!
340              
341             =head2 C
342              
343             $order->submit( );
344              
345             Use this to finally submit the order. On success, your builder is replaced by a
346             new Finance::Robinhood::Forex::Order object is returned. On failure, your
347             builder object is replaced by a Finance::Robinhood::Error object.
348              
349             =cut
350              
351 0     0 1 0 sub submit ($s) {
  0         0  
  0         0  
352 0         0 my $res = $s->_rh->_post( 'https://nummus.robinhood.com/orders/', $s->_dump );
353             $_[0]
354             = $res->is_success
355 0 0       0 ? Finance::Robinhood::Forex::Order->new( _rh => $s->_rh, %{ $res->json } )
  0 0       0  
356             : Finance::Robinhood::Error->new(
357             $res->is_server_error ? ( details => $res->message ) : $res->json );
358             }
359              
360             sub _test_submit {
361 1   50 1   1891 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
362              
363             # TODO: Skp these tests if we don't have enough cash on hand.
364 0         0 my $ask = t::Utility::stash('BTC_USD')->quote->ask_price;
365              
366             # Orders must be within 10% of ask/bid
367 0         0 my $order = t::Utility::stash('BTC_USD')->buy(.001)
368             ->gtc->limit( sprintf '%.2f', $ask - ( $ask * .1 ) );
369              
370 0         0 isa_ok( $order->submit, 'Finance::Robinhood::Forex::Order' );
371              
372             #use Data::Dump;
373             #ddx $order;
374 0         0 $order->cancel;
375             }
376              
377             # Do it! (And debug it...)
378 0     0   0 sub _dump ( $s, $test = 0 ) {
  0         0  
  0         0  
  0         0  
379             ( # Defaults
380 0 0       0 quantity => $s->quantity,
    0          
381             type => 'market',
382             currency_pair_id => $s->_pair->id,
383             account => $test ? '--private--' : $s->_account->id,
384             time_in_force => 'gtc',
385             ref_id => $test ? '00000000-0000-0000-0000-000000000000' : _gen_uuid()
386             )
387             }
388              
389 1     1   3 sub _gen_uuid() {
  1         1  
390 1         3 CORE::state $srand;
391 1 50       47 $srand = srand() if !$srand;
392             my $retval = join '', map {
393 1         6 pack 'I',
  4         23  
394             ( int( rand(0x10000) ) % 0x10000 << 0x10 ) | int( rand(0x10000) ) % 0x10000
395             } 1 .. 4;
396 1         6 substr $retval, 6, 1, chr( ord( substr( $retval, 6, 1 ) ) & 0x0f | 0x40 ); # v4
397 1         4 return join '-', map { unpack 'H*', $_ } map { substr $retval, 0, $_, '' } ( 4, 2, 2, 2, 6 );
  5         22  
  5         12  
398             }
399              
400             sub _test__gen_uuid {
401 1     1   11268 like( _gen_uuid(), qr[^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}\-[0-9a-f]{12}$]i, 'generated uuid' );
402             }
403              
404             =head1 LEGAL
405              
406             This is a simple wrapper around the API used in the official apps. The author
407             provides no investment, legal, or tax advice and is not responsible for any
408             damages incurred while using this software. This software is not affiliated
409             with Robinhood Financial LLC in any way.
410              
411             For Robinhood's terms and disclosures, please see their website at
412             https://robinhood.com/legal/
413              
414             =head1 LICENSE
415              
416             Copyright (C) Sanko Robinson.
417              
418             This library is free software; you can redistribute it and/or modify it under
419             the terms found in the Artistic License 2. Other copyrights, terms, and
420             conditions may apply to data transmitted through this module. Please refer to
421             the L section.
422              
423             =head1 AUTHOR
424              
425             Sanko Robinson Esanko@cpan.orgE
426              
427             =cut
428              
429             1;