File Coverage

lib/Finance/Robinhood/Forex/OrderBuilder.pm
Criterion Covered Total %
statement 35 95 36.8
branch 0 8 0.0
condition 7 14 50.0
subroutine 17 25 68.0
pod 7 7 100.0
total 66 149 44.3


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_003';
45 1     1   6 use Mojo::Base-base, -signatures;
  1         3  
  1         8  
46 1     1   664 use Finance::Robinhood::Forex::Order;
  1         4  
  1         8  
47 1     1   43 use Finance::Robinhood::Utilities qw[gen_uuid];
  1         3  
  1         341  
48              
49             sub _test__init {
50 1     1   11516 my $rh = t::Utility::rh_instance(1);
51 0         0 my $btc_usd
52             = $rh->forex_pair_by_id('3d961844-d360-45fc-989b-f6fca761d511');
53 0         0 t::Utility::stash('BTC_USD', $btc_usd); # Store it for later
54 0         0 isa_ok($btc_usd->buy(3), __PACKAGE__);
55 0         0 isa_ok($btc_usd->sell(3), __PACKAGE__);
56             }
57             #
58             has _rh => undef => weak => 1;
59              
60             =head1 METHODS
61              
62              
63             =head2 C
64              
65             Expects a Finance::Robinhood::Forex::Account object.
66              
67             =head2 C
68              
69             Expects a Finance::Robinhood::Forex::Pair object.
70              
71             =head2 C
72              
73             Expects a whole number of shares.
74              
75             =cut
76              
77             has _account => undef; # => weak => 1;
78             has _pair => undef; # => weak => 1;
79             has ['quantity', 'price'];
80             #
81             # Type
82              
83             =head2 C
84              
85             $order->limit( 17.98 );
86              
87             Expects a price.
88              
89             Use this to create limit and stop limit orders.
90              
91             =head2 C
92              
93             $order->market( );
94              
95             Use this to create market and stop loss orders.
96              
97             =cut
98              
99 0     0 1 0 sub limit ($s, $price) {
  0         0  
  0         0  
  0         0  
100 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Limit')
101             ->limit($price);
102             }
103             {
104              
105             package Finance::Robinhood::Forex::OrderBuilder::Role::Limit;
106 1     1   9 use Mojo::Base-role, -signatures;
  1         3  
  1         6  
107             has limit => 0;
108             around _dump => sub ($orig, $s, $test = 0) {
109             my %data = $orig->($s, $test);
110             (%data, price => $s->limit, type => 'limit');
111             };
112             }
113              
114             sub _test_limit {
115 1   50 1   1901 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
116 0         0 my $order = t::Utility::stash('BTC_USD')->buy(3)->limit(3.40);
117 0         0 is( {$order->_dump(1)},
118             {account => "--private--",
119             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
120             price => 3.40,
121             quantity => 3,
122             ref_id => "00000000-0000-0000-0000-000000000000",
123             side => "buy",
124             time_in_force => "gtc",
125             type => "limit",
126             },
127             'dump is correct'
128             );
129             }
130              
131 0     0 1 0 sub market($s) {
  0         0  
  0         0  
132 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Market');
133             }
134             {
135              
136             package Finance::Robinhood::Forex::OrderBuilder::Role::Market;
137 1     1   738 use Mojo::Base-role, -signatures;
  1         2  
  1         13  
138             around _dump => sub ($orig, $s, $test = 0) {
139             my %data = $orig->($s, $test);
140             (%data, type => 'market');
141             };
142             }
143              
144             sub _test_market {
145 1   50 1   1847 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
146 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->market();
147 0         0 is( {$order->_dump(1)},
148             {account => "--private--",
149             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
150             price => '5.00',
151             quantity => 3,
152             ref_id => "00000000-0000-0000-0000-000000000000",
153             side => "sell",
154             time_in_force => "gtc",
155             type => "market",
156             },
157             'dump is correct'
158             );
159             }
160              
161             =begin internal
162              
163             =head2 C
164              
165             $order->buy( 3 );
166              
167             Use this to change the order side.
168              
169             =head2 C
170              
171             $order->sell( 4 );
172              
173             Use this to change the order side.
174              
175             =end internal
176              
177             =cut
178              
179             # Side
180 0     0 1 0 sub buy ($s, $quantity = $s->quantity) {
  0         0  
  0         0  
  0         0  
181 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Buy');
182 0         0 $s->quantity($quantity);
183             }
184             {
185              
186             package Finance::Robinhood::Forex::OrderBuilder::Role::Buy;
187 1     1   661 use Mojo::Base-role, -signatures;
  1         2  
  1         6  
188             around _dump => sub ($orig, $s, $test = 0) {
189             my %data = $orig->($s, $test);
190             (%data,
191             side => 'buy',
192             price => $test ? '5.00' : $s->price // $s->_pair->quote->bid_price
193             );
194             };
195             }
196              
197             sub _test_buy {
198 1   50 1   1946 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
199 0         0 my $order = t::Utility::stash('BTC_USD')->sell(32)->buy(3);
200 0         0 is( {$order->_dump(1)},
201             {account => "--private--",
202             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
203             price => '5.00',
204             quantity => 3,
205             ref_id => "00000000-0000-0000-0000-000000000000",
206             side => "buy",
207             time_in_force => "gtc",
208             type => "market",
209             },
210             'dump is correct'
211             );
212             }
213              
214 0     0 1 0 sub sell ($s, $quantity = $s->quantity) {
  0         0  
  0         0  
  0         0  
215 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::Sell');
216 0         0 $s->quantity($quantity);
217             }
218             {
219              
220             package Finance::Robinhood::Forex::OrderBuilder::Role::Sell;
221 1     1   660 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
222             around _dump => sub ($orig, $s, $test = 0) {
223             my %data = $orig->($s, $test);
224             (%data,
225             side => 'sell',
226             price => $test ? '5.00' : $s->price // $s->_pair->quote->ask_price
227             );
228             };
229             }
230              
231             sub _test_sell {
232 1   50 1   1859 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
233 0         0 my $order = t::Utility::stash('BTC_USD')->buy(32)->sell(3);
234 0         0 is( {$order->_dump(1)},
235             {account => "--private--",
236             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
237             price => '5.00',
238             quantity => 3,
239             ref_id => "00000000-0000-0000-0000-000000000000",
240             side => "sell",
241             time_in_force => "gtc",
242             type => "market",
243             },
244             'dump is correct'
245             );
246             }
247              
248             # Time in force
249              
250             =head2 C
251              
252             $order->gtc( );
253              
254             Use this to change the order's time in force value to Good-Till-Cancelled
255             (actually 90 days from submission).
256              
257              
258             =head2 C
259              
260             $order->ioc( );
261              
262             Use this to change the order's time in force value to Immediate-Or-Cancel.
263              
264             This may require special permissions.
265              
266             =cut
267              
268 0     0 1 0 sub gtc($s) {
  0         0  
  0         0  
269 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::GTC');
270             }
271             {
272              
273             package Finance::Robinhood::Forex::OrderBuilder::Role::GTC;
274 1     1   696 use Mojo::Base-role, -signatures;
  1         1  
  1         5  
275             around _dump => sub ($orig, $s, $test = 0) {
276             my %data = $orig->($s, $test);
277             (%data, time_in_force => 'gtc');
278             };
279             }
280              
281             sub _test_gtc {
282 1   50 1   1921 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
283 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->gtc();
284 0         0 is( {$order->_dump(1)},
285             {account => "--private--",
286             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
287             price => '5.00',
288             quantity => 3,
289             ref_id => "00000000-0000-0000-0000-000000000000",
290             side => "sell",
291             time_in_force => "gtc",
292             type => "market",
293             },
294             'dump is correct'
295             );
296             }
297              
298 0     0 1 0 sub ioc($s) {
  0         0  
  0         0  
299 0         0 $s->with_roles('Finance::Robinhood::Forex::OrderBuilder::Role::IOC');
300             }
301             {
302              
303             package Finance::Robinhood::Forex::OrderBuilder::Role::IOC;
304 1     1   626 use Mojo::Base-role, -signatures;
  1         2  
  1         6  
305             around _dump => sub ($orig, $s, $test = 0) {
306             my %data = $orig->($s, $test);
307             (%data, time_in_force => 'ioc');
308             };
309             }
310              
311             sub _test_ioc {
312 1   50 1   1846 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
313 0         0 my $order = t::Utility::stash('BTC_USD')->sell(3)->ioc();
314 0         0 is( {$order->_dump(1)},
315             {account => "--private--",
316             currency_pair_id => "3d961844-d360-45fc-989b-f6fca761d511",
317             price => '5.00',
318             quantity => 3,
319             ref_id => "00000000-0000-0000-0000-000000000000",
320             side => "sell",
321             time_in_force => "ioc",
322             type => "market",
323             },
324             'dump is correct'
325             );
326             }
327              
328             # Do it!
329              
330             =head2 C
331              
332             $order->submit( );
333              
334             Use this to finally submit the order. On success, your builder is replaced by a
335             new Finance::Robinhood::Forex::Order object is returned. On failure, your
336             builder object is replaced by a Finance::Robinhood::Error object.
337              
338             =cut
339              
340 0     0 1 0 sub submit ($s) {
  0         0  
  0         0  
341 0         0 my $res
342             = $s->_rh->_post('https://nummus.robinhood.com/orders/', $s->_dump);
343             $_[0]
344             = $res->is_success
345 0 0       0 ? Finance::Robinhood::Forex::Order->new(_rh => $s->_rh, %{$res->json})
  0 0       0  
346             : Finance::Robinhood::Error->new(
347             $res->is_server_error ? (details => $res->message) : $res->json);
348             }
349              
350             sub _test_submit {
351 1   50 1   1875 t::Utility::stash('BTC_USD') // skip_all('No cached currency pair');
352              
353             # TODO: Skp these tests if we don't have enough cash on hand.
354 0           my $ask = t::Utility::stash('BTC_USD')->quote->ask_price;
355              
356             # Orders must be within 10% of ask/bid
357 0           my $order = t::Utility::stash('BTC_USD')->buy(.001)
358             ->gtc->limit(sprintf '%.2f', $ask - ($ask * .1));
359 0           isa_ok($order->submit, 'Finance::Robinhood::Forex::Order');
360              
361             #use Data::Dump;
362             #ddx $order;
363 0           $order->cancel;
364             }
365              
366             # Do it! (And debug it...)
367 0     0     sub _dump ($s, $test = 0) {
  0            
  0            
  0            
368             ( # Defaults
369 0 0         quantity => $s->quantity,
    0          
370             type => 'market',
371             currency_pair_id => $s->_pair->id,
372             account => $test ? '--private--' : $s->_account->id,
373             time_in_force => 'gtc',
374             ref_id => $test ? '00000000-0000-0000-0000-000000000000' : gen_uuid()
375             )
376             }
377              
378             =head1 LEGAL
379              
380             This is a simple wrapper around the API used in the official apps. The author
381             provides no investment, legal, or tax advice and is not responsible for any
382             damages incurred while using this software. This software is not affiliated
383             with Robinhood Financial LLC in any way.
384              
385             For Robinhood's terms and disclosures, please see their website at
386             https://robinhood.com/legal/
387              
388             =head1 LICENSE
389              
390             Copyright (C) Sanko Robinson.
391              
392             This library is free software; you can redistribute it and/or modify it under
393             the terms found in the Artistic License 2. Other copyrights, terms, and
394             conditions may apply to data transmitted through this module. Please refer to
395             the L section.
396              
397             =head1 AUTHOR
398              
399             Sanko Robinson Esanko@cpan.orgE
400              
401             =cut
402              
403             1;