File Coverage

lib/Finance/Robinhood/Equity/OrderBuilder.pm
Criterion Covered Total %
statement 68 204 33.3
branch 0 10 0.0
condition 16 35 45.7
subroutine 34 50 68.0
pod 15 15 100.0
total 133 314 42.3


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