File Coverage

lib/Finance/Robinhood/Equity/OrderBuilder.pm
Criterion Covered Total %
statement 76 212 35.8
branch 1 12 8.3
condition 16 35 45.7
subroutine 35 51 68.6
pod 15 15 100.0
total 143 325 44.0


line stmt bran cond sub pod time code
1             package Finance::Robinhood::Equity::OrderBuilder;
2              
3             =encoding utf-8
4              
5             =for stopwords watchlist watchlists untradable urls
6              
7             =head1 NAME
8              
9             Finance::Robinhood::Equity::OrderBuilder - Provides a Sugary Builder-type
10             Interface for Generating an Equity Order
11              
12             =head1 SYNOPSIS
13              
14             use Finance::Robinhood;
15             my $rh = Finance::Robinhood->new;
16             my $msft = $rh->instruments_by_symbol('MSFT');
17              
18             # This package isn't used directly; instead, try this...
19             my $order = $msft->buy(3)->post;
20              
21             =head1 DESCRIPTION
22              
23             This is cotton candy for creating valid order structures.
24              
25             Without any additional method calls, this will create a simple market order
26             that looks like this:
27              
28             {
29             account => "https://api.robinhood.com/accounts/XXXXXXXXXX/",
30             instrument => "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
31             price => "111.700000", # Automatically grabs last trade price quote
32             quantity => 4, # Actually the number of shares you requested
33             side => "buy", # Or sell
34             symbol => "MSFT", # Grabs ticker symbol automatically from instrument object
35             time_in_force => "gfd",
36             trigger => "immediate",
37             type => "market"
38             }
39              
40             You may chain together several methods to generate and submit advanced order
41             types such as stop limits that are held up to 90 days:
42              
43             $order->stop(24.50)->gtc->limit->submit;
44              
45             =cut
46              
47             our $VERSION = '0.92_001';
48 1     1   10 use Mojo::Base-base, -signatures;
  1         2  
  1         7  
49 1     1   648 use Finance::Robinhood::Equity::Order;
  1         4  
  1         8  
50              
51             sub _test__init {
52 1     1   2541 my $rh = t::Utility::rh_instance(1);
53 0         0 my $msft = $rh->equity_instrument_by_symbol('MSFT');
54 0         0 t::Utility::stash( 'MSFT', $msft ); # Store it for later
55 0         0 isa_ok( $msft->buy(3), __PACKAGE__ );
56 0         0 isa_ok( $msft->sell(3), __PACKAGE__ );
57             }
58             #
59             has _rh => undef => weak => 1;
60              
61             =head1 METHODS
62              
63              
64             =head2 C
65              
66             Expects a Finance::Robinhood::Equity::Account object.
67              
68             =head2 C
69              
70             Expects a Finance::Robinhood::Equity::Instrument object.
71              
72             =head2 C
73              
74             Expects a whole number of shares.
75              
76             =cut
77              
78             has _account => undef; # => weak => 1;
79             has _instrument => undef; # => weak => 1;
80             has [ 'quantity', 'price' ];
81             #
82              
83             =head2 C
84              
85             $order->stop( 45.20 );
86              
87             Expects a price.
88              
89             Use this to create stop limit or stop loss orders.
90              
91             =cut
92              
93 0     0 1 0 sub stop ( $s, $price ) {
  0         0  
  0         0  
  0         0  
94 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::Stop')->stop($price);
95             }
96             {
97              
98             package Finance::Robinhood::Equity::OrderBuilder::Role::Stop;
99 1     1   237 use Mojo::Base-role, -signatures;
  1         2  
  1         6  
100             has stop => 0;
101             around _dump => sub ( $orig, $s, $test = 0 ) {
102             my %data = $orig->( $s, $test );
103             ( %data, stop_price => $s->stop, trigger => 'stop' );
104             };
105             1;
106             }
107              
108             sub _test_stop {
109 1   50 1   1868 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
110 0         0 my $order = t::Utility::stash('MSFT')->buy(3)->stop(3.40);
111 0         0 is(
112             { $order->_dump(1) },
113             {
114             account => "--private--",
115             instrument =>
116             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
117             price => "5.00",
118             quantity => 3,
119             ref_id => "00000000-0000-0000-0000-000000000000",
120             side => "buy",
121             symbol => "MSFT",
122             time_in_force => "gfd",
123             trigger => "stop",
124             stop_price => 3.40,
125             type => "market",
126             },
127             'dump is correct'
128             );
129             }
130              
131             # Type
132              
133             =head2 C
134              
135             $order->limit( 17.98 );
136              
137             Expects a price.
138              
139             Use this to create limit and stop limit orders.
140              
141             =head2 C
142              
143             $order->market( );
144              
145             Use this to create market and stop loss orders.
146              
147             =cut
148              
149 0     0 1 0 sub limit ( $s, $price ) {
  0         0  
  0         0  
  0         0  
150 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::Limit')->limit($price);
151             }
152             {
153              
154             package Finance::Robinhood::Equity::OrderBuilder::Role::Limit;
155 1     1   706 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
156             has limit => 0;
157             around _dump => sub ( $orig, $s, $test = 0 ) {
158             my %data = $orig->( $s, $test );
159             ( %data, price => $s->limit, type => 'limit' );
160             };
161             }
162              
163             sub _test_limit {
164 1   50 1   1819 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
165 0         0 my $order = t::Utility::stash('MSFT')->buy(3)->limit(3.40);
166 0         0 is(
167             { $order->_dump(1) },
168             {
169             account => "--private--",
170             instrument =>
171             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
172             price => 3.40,
173             quantity => 3,
174             ref_id => "00000000-0000-0000-0000-000000000000",
175             side => "buy",
176             symbol => "MSFT",
177             time_in_force => "gfd",
178             trigger => "immediate",
179             type => "limit",
180             },
181             'dump is correct'
182             );
183             }
184              
185 0     0 1 0 sub market($s) {
  0         0  
  0         0  
186 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::Market');
187             }
188             {
189              
190             package Finance::Robinhood::Equity::OrderBuilder::Role::Market;
191 1     1   600 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
192             around _dump => sub ( $orig, $s, $test = 0 ) {
193             my %data = $orig->( $s, $test );
194             ( %data, type => 'market' );
195             };
196             }
197              
198             sub _test_market {
199 1   50 1   1896 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
200 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->market();
201 0         0 is(
202             { $order->_dump(1) },
203             {
204             account => "--private--",
205             instrument =>
206             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
207             price => '5.00',
208             quantity => 3,
209             ref_id => "00000000-0000-0000-0000-000000000000",
210             side => "sell",
211             symbol => "MSFT",
212             time_in_force => "gfd",
213             trigger => "immediate",
214             type => "market",
215             },
216             'dump is correct'
217             );
218             }
219              
220             =begin internal
221              
222             =head2 C
223              
224             $order->buy( 3 );
225              
226             Use this to change the order side.
227              
228             =head2 C
229              
230             $order->sell( 4 );
231              
232             Use this to change the order side.
233              
234             =end internal
235              
236             =cut
237              
238             # Side
239 0     0 1 0 sub buy ( $s, $quantity = $s->quantity ) {
  0         0  
  0         0  
  0         0  
240 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::Buy');
241 0         0 $s->quantity($quantity);
242             }
243             {
244              
245             package Finance::Robinhood::Equity::OrderBuilder::Role::Buy;
246 1     1   675 use Mojo::Base-role, -signatures;
  1         3  
  1         6  
247             around _dump => sub ( $orig, $s, $test = 0 ) {
248             my %data = $orig->( $s, $test );
249             ( %data, side => 'buy' );
250             };
251             }
252              
253             sub _test_buy {
254 1   50 1   1950 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
255 0         0 my $order = t::Utility::stash('MSFT')->sell(32)->buy(3);
256 0         0 is(
257             { $order->_dump(1) },
258             {
259             account => "--private--",
260             instrument =>
261             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
262             price => '5.00',
263             quantity => 3,
264             ref_id => "00000000-0000-0000-0000-000000000000",
265             side => "buy",
266             symbol => "MSFT",
267             time_in_force => "gfd",
268             trigger => "immediate",
269             type => "market",
270             },
271             'dump is correct'
272             );
273             }
274              
275 0     0 1 0 sub sell ( $s, $quantity = $s->quantity ) {
  0         0  
  0         0  
  0         0  
276 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::Sell');
277 0         0 $s->quantity($quantity);
278             }
279             {
280              
281             package Finance::Robinhood::Equity::OrderBuilder::Role::Sell;
282 1     1   656 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
283             around _dump => sub ( $orig, $s, $test = 0 ) {
284             my %data = $orig->( $s, $test );
285             ( %data, side => 'sell' );
286             };
287             }
288              
289             sub _test_sell {
290 1   50 1   1815 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
291 0         0 my $order = t::Utility::stash('MSFT')->buy(32)->sell(3);
292 0         0 is(
293             { $order->_dump(1) },
294             {
295             account => "--private--",
296             instrument =>
297             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
298             price => '5.00',
299             quantity => 3,
300             ref_id => "00000000-0000-0000-0000-000000000000",
301             side => "sell",
302             symbol => "MSFT",
303             time_in_force => "gfd",
304             trigger => "immediate",
305             type => "market",
306             },
307             'dump is correct'
308             );
309             }
310              
311             # Time in force
312              
313             =head2 C
314              
315             $order->gfd( );
316              
317             Use this to change the order's time in force value to Good-For-Day.
318              
319             =head2 C
320              
321             $order->gtc( );
322              
323             Use this to change the order's time in force value to Good-Till-Cancelled
324             (actually 90 days from submission).
325              
326             =head2 C
327              
328             $order->fok( );
329              
330             Use this to change the order's time in force value to Fill-Or-Kill.
331              
332             This may require special permissions.
333              
334             =head2 C
335              
336             $order->ioc( );
337              
338             Use this to change the order's time in force value to Immediate-Or-Cancel.
339              
340             This may require special permissions.
341              
342             =head2 C
343              
344             $order->opg( );
345              
346             Use this to change the order's time in force value to Market-On-Open or
347             Limit-On-Open orders.
348              
349             This is not valid for orders marked for execution during extended hours.
350              
351             =cut
352              
353 0     0 1 0 sub gfd($s) {
  0         0  
  0         0  
354 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::GFD');
355             }
356             {
357              
358             package Finance::Robinhood::Equity::OrderBuilder::Role::GFD;
359 1     1   637 use Mojo::Base-role, -signatures;
  1         2  
  1         4  
360             around _dump => sub ( $orig, $s, $test = 0 ) {
361             my %data = $orig->( $s, $test );
362             ( %data, time_in_force => 'gfd' );
363             };
364             }
365              
366             sub _test_gfd {
367 1   50 1   1894 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
368 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->gfd();
369 0         0 is(
370             { $order->_dump(1) },
371             {
372             account => "--private--",
373             instrument =>
374             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
375             price => '5.00',
376             quantity => 3,
377             ref_id => "00000000-0000-0000-0000-000000000000",
378             side => "sell",
379             symbol => "MSFT",
380             time_in_force => "gfd",
381             trigger => "immediate",
382             type => "market",
383             },
384             'dump is correct'
385             );
386             }
387              
388 0     0 1 0 sub gtc($s) {
  0         0  
  0         0  
389 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::GTC');
390             }
391             {
392              
393             package Finance::Robinhood::Equity::OrderBuilder::Role::GTC;
394 1     1   579 use Mojo::Base-role, -signatures;
  1         3  
  1         5  
395             around _dump => sub ( $orig, $s, $test = 0 ) {
396             my %data = $orig->( $s, $test );
397             ( %data, time_in_force => 'gtc' );
398             };
399             }
400              
401             sub _test_gtc {
402 1   50 1   1854 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
403 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->gtc();
404 0         0 is(
405             { $order->_dump(1) },
406             {
407             account => "--private--",
408             instrument =>
409             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
410             price => '5.00',
411             quantity => 3,
412             ref_id => "00000000-0000-0000-0000-000000000000",
413             side => "sell",
414             symbol => "MSFT",
415             time_in_force => "gtc",
416             trigger => "immediate",
417             type => "market",
418             },
419             'dump is correct'
420             );
421             }
422              
423 0     0 1 0 sub fok($s) {
  0         0  
  0         0  
424 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::FOK');
425             }
426             {
427              
428             package Finance::Robinhood::Equity::OrderBuilder::Role::FOK;
429 1     1   618 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
430             around _dump => sub ( $orig, $s, $test = 0 ) {
431             my %data = $orig->( $s, $test );
432             ( %data, time_in_force => 'fok' );
433             };
434             }
435              
436             sub _test_fok {
437 1   50 1   1848 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
438 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->fok();
439 0         0 is(
440             { $order->_dump(1) },
441             {
442             account => "--private--",
443             instrument =>
444             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
445             price => '5.00',
446             quantity => 3,
447             ref_id => "00000000-0000-0000-0000-000000000000",
448             side => "sell",
449             symbol => "MSFT",
450             time_in_force => "fok",
451             trigger => "immediate",
452             type => "market",
453             },
454             'dump is correct'
455             );
456             }
457              
458 0     0 1 0 sub ioc($s) {
  0         0  
  0         0  
459 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::IOC');
460             }
461             {
462              
463             package Finance::Robinhood::Equity::OrderBuilder::Role::IOC;
464 1     1   608 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
465             around _dump => sub ( $orig, $s, $test = 0 ) {
466             my %data = $orig->( $s, $test );
467             ( %data, time_in_force => 'ioc' );
468             };
469             }
470              
471             sub _test_ioc {
472 1   50 1   1873 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
473 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->ioc();
474 0         0 is(
475             { $order->_dump(1) },
476             {
477             account => "--private--",
478             instrument =>
479             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
480             price => '5.00',
481             quantity => 3,
482             ref_id => "00000000-0000-0000-0000-000000000000",
483             side => "sell",
484             symbol => "MSFT",
485             time_in_force => "ioc",
486             trigger => "immediate",
487             type => "market",
488             },
489             'dump is correct'
490             );
491             }
492              
493 0     0 1 0 sub opg($s) {
  0         0  
  0         0  
494 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::OPG');
495             }
496             {
497              
498             package Finance::Robinhood::Equity::OrderBuilder::Role::OPG;
499 1     1   621 use Mojo::Base-role, -signatures;
  1         2  
  1         4  
500             around _dump => sub ( $orig, $s, $test = 0 ) {
501             my %data = $orig->( $s, $test );
502             ( %data, time_in_force => 'opg' );
503             };
504             }
505              
506             sub _test_opg {
507 1   50 1   1820 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
508 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->opg();
509 0         0 is(
510             { $order->_dump(1) },
511             {
512             account => "--private--",
513             instrument =>
514             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
515             price => '5.00',
516             quantity => 3,
517             ref_id => "00000000-0000-0000-0000-000000000000",
518             side => "sell",
519             symbol => "MSFT",
520             time_in_force => "opg",
521             trigger => "immediate",
522             type => "market",
523             },
524             'dump is correct'
525             );
526             }
527              
528             # Bonus!
529              
530             =head2 C
531              
532             $order->pre_ipo( );
533              
534             Enables special pre-IPO submission of orders.
535              
536             $order->pre_ipo( 1 );
537             $order->pre_ipo( 0 );
538              
539             Enable or disables pre-IPO submission of orders.
540              
541             =cut
542              
543 0     0 1 0 sub pre_ipo ( $s, $bool = 1 ) {
  0         0  
  0         0  
  0         0  
544 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::IPO');
545 0         0 $s->_pre_ipo($bool);
546             }
547             {
548              
549             package Finance::Robinhood::Equity::OrderBuilder::Role::IPO;
550 1     1   642 use Mojo::Base-role, -signatures;
  1         3  
  1         5  
551             has _pre_ipo => 1;
552             around _dump => sub ( $orig, $s, $test = 0 ) {
553             my %data = $orig->( $s, $test );
554             ( %data, isPreIpo => $s->_pre_ipo ? 'true' : 'false' );
555             };
556             }
557              
558             sub _test_pre_ipo {
559 1   50 1   1886 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
560 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->pre_ipo();
561 0         0 is(
562             { $order->_dump(1) },
563             {
564             account => "--private--",
565             instrument =>
566             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
567             price => '5.00',
568             quantity => 3,
569             ref_id => "00000000-0000-0000-0000-000000000000",
570             side => "sell",
571             symbol => "MSFT",
572             time_in_force => "gfd",
573             trigger => "immediate",
574             type => "market",
575             isPreIpo => 'true'
576             },
577             'dump is correct (default)'
578             );
579             #
580 0         0 $order = t::Utility::stash('MSFT')->sell(3)->pre_ipo(1);
581 0         0 is(
582             { $order->_dump(1) },
583             {
584             account => "--private--",
585             instrument =>
586             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
587             price => '5.00',
588             quantity => 3,
589             ref_id => "00000000-0000-0000-0000-000000000000",
590             side => "sell",
591             symbol => "MSFT",
592             time_in_force => "gfd",
593             trigger => "immediate",
594             type => "market",
595             isPreIpo => 'true'
596             },
597             'dump is correct (true)'
598             );
599             #
600 0         0 $order = t::Utility::stash('MSFT')->sell(3)->pre_ipo(0);
601 0         0 is(
602             { $order->_dump(1) },
603             {
604             account => "--private--",
605             instrument =>
606             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
607             price => '5.00',
608             quantity => 3,
609             ref_id => "00000000-0000-0000-0000-000000000000",
610             side => "sell",
611             symbol => "MSFT",
612             time_in_force => "gfd",
613             trigger => "immediate",
614             type => "market",
615             isPreIpo => 'false'
616             },
617             'dump is correct (false)'
618             );
619             }
620              
621             =head2 C
622              
623             $order->override_day_trade_checks( );
624              
625             Disables server side checks for possible day trade violations.
626              
627             $order->override_day_trade_checks( 1 );
628             $order->override_day_trade_checks( 0 );
629              
630             Enables or disables server side checks for possible day trade violations.
631              
632             =cut
633              
634 0     0 1 0 sub override_day_trade_checks ( $s, $bool = 1 ) {
  0         0  
  0         0  
  0         0  
635 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::IgnoreDT');
636 0         0 $s->_overridePDT($bool);
637             }
638             {
639              
640             package Finance::Robinhood::Equity::OrderBuilder::Role::IgnoreDT;
641 1     1   838 use Mojo::Base-role, -signatures;
  1         2  
  1         4  
642             has _overridePDT => 1;
643             around _dump => sub ( $orig, $s, $test = 0 ) {
644             my %data = $orig->( $s, $test );
645             ( %data, override_day_trade_checks => $s->_overridePDT ? 'true' : 'false' );
646             };
647             }
648              
649             sub _test_override_day_trade_checks {
650 1   50 1   1882 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
651 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->override_day_trade_checks();
652 0         0 is(
653             { $order->_dump(1) },
654             {
655             account => "--private--",
656             instrument =>
657             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
658             price => '5.00',
659             quantity => 3,
660             ref_id => "00000000-0000-0000-0000-000000000000",
661             side => "sell",
662             symbol => "MSFT",
663             time_in_force => "gfd",
664             trigger => "immediate",
665             type => "market",
666             override_day_trade_checks => 'true'
667             },
668             'dump is correct (default)'
669             );
670             #
671 0         0 $order = t::Utility::stash('MSFT')->sell(3)->override_day_trade_checks(1);
672 0         0 is(
673             { $order->_dump(1) },
674             {
675             account => "--private--",
676             instrument =>
677             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
678             price => '5.00',
679             quantity => 3,
680             ref_id => "00000000-0000-0000-0000-000000000000",
681             side => "sell",
682             symbol => "MSFT",
683             time_in_force => "gfd",
684             trigger => "immediate",
685             type => "market",
686             override_day_trade_checks => 'true'
687             },
688             'dump is correct (true)'
689             );
690             #
691 0         0 $order = t::Utility::stash('MSFT')->sell(3)->override_day_trade_checks(0);
692 0         0 is(
693             { $order->_dump(1) },
694             {
695             account => "--private--",
696             instrument =>
697             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
698             price => '5.00',
699             quantity => 3,
700             ref_id => "00000000-0000-0000-0000-000000000000",
701             side => "sell",
702             symbol => "MSFT",
703             time_in_force => "gfd",
704             trigger => "immediate",
705             type => "market",
706             override_day_trade_checks => 'false'
707             },
708             'dump is correct (false)'
709             );
710             }
711              
712             =head2 C
713              
714             $order->override_dtbp_checks( );
715              
716             Disables server side checks for possible day trade buying power violations.
717              
718             $order->override_dtbp_checks( 1 );
719             $order->override_dtbp_checks( 0 );
720              
721             Enables or disables server side checks for possible day trade buying power
722             violations.
723              
724             =cut
725              
726 0     0 1 0 sub override_dtbp_checks ( $s, $bool = 1 ) {
  0         0  
  0         0  
  0         0  
727 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::IgnoreDTBP');
728 0         0 $s->_ignore_dtbp($bool);
729             }
730             {
731              
732             package Finance::Robinhood::Equity::OrderBuilder::Role::IgnoreDTBP;
733 1     1   736 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
734             has _ignore_dtbp => 1;
735             around _dump => sub ( $orig, $s, $test = 0 ) {
736             my %data = $orig->( $s, $test );
737             ( %data, override_dtbp_checks => $s->_ignore_dtbp ? 'true' : 'false' );
738             };
739             }
740              
741             sub _test_override_dtbp_checks {
742 1   50 1   1824 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
743 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->override_dtbp_checks();
744 0         0 is(
745             { $order->_dump(1) },
746             {
747             account => "--private--",
748             instrument =>
749             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
750             price => '5.00',
751             quantity => 3,
752             ref_id => "00000000-0000-0000-0000-000000000000",
753             side => "sell",
754             symbol => "MSFT",
755             time_in_force => "gfd",
756             trigger => "immediate",
757             type => "market",
758             override_dtbp_checks => 'true'
759             },
760             'dump is correct (default)'
761             );
762             #
763 0         0 $order = t::Utility::stash('MSFT')->sell(3)->override_dtbp_checks(1);
764 0         0 is(
765             { $order->_dump(1) },
766             {
767             account => "--private--",
768             instrument =>
769             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
770             price => '5.00',
771             quantity => 3,
772             ref_id => "00000000-0000-0000-0000-000000000000",
773             side => "sell",
774             symbol => "MSFT",
775             time_in_force => "gfd",
776             trigger => "immediate",
777             type => "market",
778             override_dtbp_checks => 'true'
779             },
780             'dump is correct (true)'
781             );
782             #
783 0         0 $order = t::Utility::stash('MSFT')->sell(3)->override_dtbp_checks(0);
784 0         0 is(
785             { $order->_dump(1) },
786             {
787             account => "--private--",
788             instrument =>
789             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
790             price => '5.00',
791             quantity => 3,
792             ref_id => "00000000-0000-0000-0000-000000000000",
793             side => "sell",
794             symbol => "MSFT",
795             time_in_force => "gfd",
796             trigger => "immediate",
797             type => "market",
798             override_dtbp_checks => 'false'
799             },
800             'dump is correct (false)'
801             );
802             }
803              
804             =head2 C
805              
806             $order->extended_hours( )
807              
808             Enables order execution during pre- and after-hours.
809              
810             $order->extended_hours( 1 );
811             $order->extended_hours( 0 );
812              
813             Enables or disables execution during pre- and after-hours.
814              
815             Note that the market orders may be converted to a limit orders (at or near the
816             current price) by the API server's back end. You would be wise to set your own
817             limit price instead.
818              
819             =cut
820              
821 0     0 1 0 sub extended_hours ( $s, $bool = 1 ) {
  0         0  
  0         0  
  0         0  
822 0         0 $s->with_roles('Finance::Robinhood::Equity::OrderBuilder::Role::ExtHours');
823 0         0 $s->_ext_hrs($bool);
824             }
825             {
826              
827             package Finance::Robinhood::Equity::OrderBuilder::Role::ExtHours;
828 1     1   750 use Mojo::Base-role, -signatures;
  1         2  
  1         5  
829             has _ext_hrs => 1;
830             around _dump => sub ( $orig, $s, $test = 0 ) {
831             my %data = $orig->( $s, $test );
832             ( %data, extended_hours => $s->_ext_hrs ? 'true' : 'false' );
833             };
834             }
835              
836             sub _test_extended_hours {
837 1   50 1   1878 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
838 0         0 my $order = t::Utility::stash('MSFT')->sell(3)->extended_hours();
839 0         0 is(
840             { $order->_dump(1) },
841             {
842             account => "--private--",
843             instrument =>
844             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
845             price => '5.00',
846             quantity => 3,
847             ref_id => "00000000-0000-0000-0000-000000000000",
848             side => "sell",
849             symbol => "MSFT",
850             time_in_force => "gfd",
851             trigger => "immediate",
852             type => "market",
853             extended_hours => 'true'
854             },
855             'dump is correct (default)'
856             );
857             #
858 0         0 $order = t::Utility::stash('MSFT')->sell(3)->extended_hours(1);
859 0         0 is(
860             { $order->_dump(1) },
861             {
862             account => "--private--",
863             instrument =>
864             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
865             price => '5.00',
866             quantity => 3,
867             ref_id => "00000000-0000-0000-0000-000000000000",
868             side => "sell",
869             symbol => "MSFT",
870             time_in_force => "gfd",
871             trigger => "immediate",
872             type => "market",
873             extended_hours => 'true'
874             },
875             'dump is correct (true)'
876             );
877             #
878 0         0 $order = t::Utility::stash('MSFT')->sell(3)->extended_hours(0);
879 0         0 is(
880             { $order->_dump(1) },
881             {
882             account => "--private--",
883             instrument =>
884             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
885             price => '5.00',
886             quantity => 3,
887             ref_id => "00000000-0000-0000-0000-000000000000",
888             side => "sell",
889             symbol => "MSFT",
890             time_in_force => "gfd",
891             trigger => "immediate",
892             type => "market",
893             extended_hours => 'false'
894             },
895             'dump is correct (false)'
896             );
897             }
898              
899             # Do it!
900              
901             =head2 C
902              
903             $order->submit( );
904              
905             Use this to finally submit the order. On success, your builder is replaced by a
906             new Finance::Robinhood::Equity::Order object is returned. On failure, your
907             builder object is replaced by a Finance::Robinhood::Error object.
908              
909             =cut
910              
911 0     0 1 0 sub submit ($s) {
  0         0  
  0         0  
912 0         0 my $res = $s->_rh->_post( 'https://api.robinhood.com/orders/', $s->_dump );
913             $_[0]
914             = $res->is_success
915 0 0       0 ? Finance::Robinhood::Equity::Order->new( _rh => $s->_rh, %{ $res->json } )
  0 0       0  
916             : Finance::Robinhood::Error->new(
917             $res->is_server_error ? ( details => $res->message ) : $res->json );
918             }
919              
920             sub _test_submit {
921 1   50 1   1820 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
922 0         0 my $order = t::Utility::stash('MSFT')->buy(4)->extended_hours->gtc->limit(4.01);
923 0         0 isa_ok( $order->submit, 'Finance::Robinhood::Equity::Order' );
924 0         0 $order->cancel;
925             }
926              
927             # Do it! (And debug it...)
928 0     0   0 sub _dump ( $s, $test = 0 ) {
  0         0  
  0         0  
  0         0  
929             ( # Defaults
930 0 0 0     0 quantity => $s->quantity,
    0          
    0          
931             trigger => 'immediate',
932             type => 'market',
933             instrument => $s->_instrument->url,
934             symbol => $s->_instrument->symbol,
935             account => $test ? '--private--' : $s->_account->url,
936             time_in_force => 'gfd',
937             price => $test ? '5.00' : ( $s->price // $s->_instrument->quote->last_trade_price ),
938             ref_id => $test ? '00000000-0000-0000-0000-000000000000' : _gen_uuid()
939             )
940             }
941              
942 1     1   2 sub _gen_uuid() {
  1         2  
943 1         2 CORE::state $srand;
944 1 50       67 $srand = srand() if !$srand;
945             my $retval = join '', map {
946 1         6 pack 'I',
  4         26  
947             ( int( rand(0x10000) ) % 0x10000 << 0x10 ) | int( rand(0x10000) ) % 0x10000
948             } 1 .. 4;
949 1         7 substr $retval, 6, 1, chr( ord( substr( $retval, 6, 1 ) ) & 0x0f | 0x40 ); # v4
950 1         3 return join '-', map { unpack 'H*', $_ } map { substr $retval, 0, $_, '' } ( 4, 2, 2, 2, 6 );
  5         22  
  5         10  
951             }
952              
953             sub _test__gen_uuid {
954 1     1   11766 like( _gen_uuid(), qr[^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}\-[0-9a-f]{12}$]i, 'generated uuid' );
955             }
956              
957             # Advanced order tests
958             sub _test_z_advanced_orders {
959 1   50 1   1850 t::Utility::stash('MSFT') // skip_all('No cached equity instrument');
960              
961             # Stop limit
962 0           my $order = t::Utility::stash('MSFT')->sell(3)->stop('4.00')->limit(3.55);
963 0           is(
964             { $order->_dump(1) },
965             {
966             account => "--private--",
967             instrument =>
968             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
969             price => '3.55',
970             stop_price => '4.00',
971             quantity => 3,
972             ref_id => "00000000-0000-0000-0000-000000000000",
973             side => "sell",
974             symbol => "MSFT",
975             time_in_force => "gfd",
976             trigger => "stop",
977             type => "limit",
978             },
979             'stop limit'
980             );
981 0           $order = t::Utility::stash('MSFT')->sell(3)->stop('4.00')->gtc;
982 0           is(
983             { $order->_dump(1) },
984             {
985             account => "--private--",
986             instrument =>
987             "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/",
988             price => '5.00',
989             stop_price => '4.00',
990             quantity => 3,
991             ref_id => "00000000-0000-0000-0000-000000000000",
992             side => "sell",
993             symbol => "MSFT",
994             time_in_force => "gtc",
995             trigger => "stop",
996             type => "market",
997             },
998             'stop loss gtc'
999             );
1000             }
1001              
1002             =head1 LEGAL
1003              
1004             This is a simple wrapper around the API used in the official apps. The author
1005             provides no investment, legal, or tax advice and is not responsible for any
1006             damages incurred while using this software. This software is not affiliated
1007             with Robinhood Financial LLC in any way.
1008              
1009             For Robinhood's terms and disclosures, please see their website at
1010             https://robinhood.com/legal/
1011              
1012             =head1 LICENSE
1013              
1014             Copyright (C) Sanko Robinson.
1015              
1016             This library is free software; you can redistribute it and/or modify it under
1017             the terms found in the Artistic License 2. Other copyrights, terms, and
1018             conditions may apply to data transmitted through this module. Please refer to
1019             the L section.
1020              
1021             =head1 AUTHOR
1022              
1023             Sanko Robinson Esanko@cpan.orgE
1024              
1025             =cut
1026              
1027             1;