File Coverage

blib/lib/Finance/StockAccount/Set.pm
Criterion Covered Total %
statement 243 269 90.3
branch 63 80 78.7
condition 26 39 66.6
subroutine 40 43 93.0
pod 27 38 71.0
total 399 469 85.0


line stmt bran cond sub pod time code
1             package Finance::StockAccount::Set;
2              
3             our $VERSION = '0.01';
4              
5 6     6   761 use strict;
  6         7  
  6         205  
6 6     6   23 use warnings;
  6         8  
  6         144  
7              
8 6     6   1249 use Time::Moment;
  6         8823  
  6         135  
9 6     6   29 use Carp;
  6         8  
  6         326  
10              
11 6     6   2558 use Finance::StockAccount::Realization;
  6         9  
  6         12265  
12              
13              
14             sub new {
15 45     45 0 48 my ($class, $init) = @_;
16 45         111 my $self = {
17             stock => undef,
18             accountTransactions => [],
19             realizations => [],
20             stats => getNewStatsHash(),
21             dateLimit => {
22             start => undef,
23             end => undef,
24             },
25             verbose => 0,
26             };
27 45         101 bless($self, $class);
28 45 50       125 $init and $self->add($init);
29 45         81 return $self;
30             }
31              
32             sub getNewStatsHash {
33             return {
34 492     492 0 2860 stale => 1,
35             success => 0,
36             profit => 0,
37             totalOutlays => 0,
38             totalRevenues => 0,
39             commissions => 0,
40             regulatoryFees => 0,
41             otherFees => 0,
42             startDate => undef,
43             endDate => undef,
44             unrealizedTransactionCount => 0,
45             };
46             }
47              
48             sub realizationCount {
49 460     460 1 475 my $self = shift;
50 460         417 return scalar(@{$self->{realizations}});
  460         1135  
51             }
52              
53             sub unrealizedTransactions {
54 1     1 1 1 my $self = shift;
55 1         2 return [grep { $_->accounted() == 0 } @{$self->{accountTransactions}}];
  5         10  
  1         2  
56             }
57              
58             sub realizedTransactions {
59 0     0 1 0 my $self = shift;
60 0         0 return [grep { $_->accounted() > 0 } @{$self->{accountTransactions}}];
  0         0  
  0         0  
61             }
62              
63             sub transactionCount {
64 174     174 1 162 my $self = shift;
65 174         172 my $count = 0;
66 174         150 foreach my $at (@{$self->{accountTransactions}}) {
  174         267  
67 1514 100       2343 $at->accounted() > 0 and $count++;
68             }
69 174         294 return $count;
70             }
71              
72             sub unrealizedTransactionCount {
73 102     102 1 92 my $self = shift;
74 102         92 my $count = 0;
75 102         82 foreach my $at (@{$self->{accountTransactions}}) {
  102         159  
76 588 100       923 $at->accounted() == 0 and $count++;
77             }
78 102         182 return $count;
79             }
80              
81             sub availableAcquisitions {
82 0     0 1 0 my $self = shift;
83 0         0 my @aa;
84 0         0 foreach my $at (@{$self->{accountTransactions}}) {
  0         0  
85 0 0 0     0 if ($at->available() and ($at->buy() or $at->short())) {
      0        
86 0         0 push(@aa, $at);
87             }
88             }
89 0         0 return \@aa;
90             }
91              
92             sub availableAcquisitionsString {
93 0     0 1 0 my $self = shift;
94 0         0 my $aa = $self->availableAcquisitions();
95 0         0 my $string;
96 0         0 my $header = Finance::StockAccount::Transaction->lineFormatHeader();
97 0         0 foreach my $at (@$aa) {
98 0         0 $string .= $at->lineFormatString(1);
99             }
100 0 0       0 if ($string) {
101 0         0 $string = $header . $string;
102             }
103             else {
104 0         0 $string = '';
105             }
106 0         0 return $string;
107             }
108              
109             sub stale {
110 1566     1566 1 1411 my ($self, $assertion) = @_;
111 1566 100       1972 if (defined($assertion)) {
112 706 50 66     1808 if ($assertion == 1 or $assertion == 0) {
113 706 100       1194 $self->{stats}{stale} = $assertion ? 1 : 0;
114 706         793 return 1;
115             }
116             else {
117 0         0 croak "Method 'stale' only accepts assertions in the form of 1 or 0 -- $assertion is not valid.";
118             }
119             }
120             else {
121 860         2112 return $self->{stats}{stale};
122             }
123             }
124              
125             sub symbol {
126 1     1 1 1 my $self = shift;
127 1         1 my $stock = $self->{stock};
128 1         8 return $stock->symbol();
129             }
130              
131             sub add {
132 260     260 0 233 my ($self, $accountTransactions) = @_;
133 260 50 33     913 ($accountTransactions and 'ARRAY' eq ref($accountTransactions))
134             or confess "Set->add([\$st1, \$st2, \$st3, ...]) ... method requires a reference to a list of st objects.\n";
135 260         268 my $set = $self->{accountTransactions};
136 260         212 my $added = 0;
137 260         274 my $stock = $self->{stock};
138 260         371 foreach my $at (@$accountTransactions) {
139 266 50       459 'Finance::StockAccount::AccountTransaction' eq ref($at) or confess "Not a valid at object.\n";
140 266 100       382 if (!$stock) {
141 45 50       97 if ($stock = $at->stock()) {
142 45         52 $self->{stock} = $stock;
143             }
144             }
145 266 50       471 $stock->same($at->stock()) or croak "Given Stock Transaction object does not match stock for set, or set stock is undefined.";
146 266         375 push(@$set, $at);
147 266         392 $added = 1;
148             }
149 260 50       416 if ($added) {
150 260         324 $self->stale(1);
151 260         256 $self->{dateSort} = 0;
152             }
153 260         415 return $added;
154             }
155              
156             sub clearPastAccounting {
157 447     447 0 358 my $self = shift;
158 447         597 my $accountTransactions = $self->{accountTransactions};
159 447         884 for (my $x=0; $x
160 2615         2362 my $at = $accountTransactions->[$x];
161 2615         4294 $at->resetAccounted();
162             }
163 447         684 $self->{realizations} = [];
164 447         1797 $self->{stats} = $self->getNewStatsHash();
165 447         1207 $self->{stats}{success} = 0;
166 447         572 return 1;
167             }
168              
169             sub setDateLimit {
170 350     350 1 2831 my ($self, $tm1, $tm2) = @_;
171 350 50       970 if ($tm1 > $tm2) {
172 0         0 croak "The start date must come before the end date.";
173             }
174 350         545 my $dateLimit = $self->{dateLimit};
175 350         388 $dateLimit->{start} = $tm1;
176 350         312 $dateLimit->{end} = $tm2;
177 350         484 $self->{stats}{stale} = 1;
178 350         505 return 1;
179             }
180              
181             sub clearDateLimit {
182 338     338 1 280 my $self = shift;
183 338         315 my $dateLimit = $self->{dateLimit};
184 338         399 $dateLimit->{start} = undef;
185 338         286 $dateLimit->{end} = undef;
186 338         344 $self->{stats}{stale} = 1;
187 338         550 return 1;
188             }
189              
190             sub cmpPrice {
191 2603     2603 0 2138 my ($self, $at1, $at2) = @_;
192 2603         2436 my $p1 = $at1->{price};
193 2603         2223 my $p2 = $at2->{price};
194 2603 100       5328 return $p1 > $p2 ? 1 :
    100          
195             $p1 == $p2 ? 0 :
196             -1;
197             }
198              
199             sub dateSort {
200 44     44 0 42 my $self = shift;
201 44         40 $self->{accountTransactions} = [sort { $a->tm() <=> $b->tm() } @{$self->{accountTransactions}}];
  314         529  
  44         138  
202 44         70 $self->{dateSort} = 1;
203 44         52 return 1;
204             }
205              
206             sub transactionDates {
207 1     1 1 1 my $self = shift;
208 1         1 my $transactionDates = [];
209 1         1 foreach my $at (@{$self->{accountTransactions}}) {
  1         2  
210 5         8 push(@$transactionDates, $at->tm());
211             }
212 1         2 return $transactionDates;
213             }
214              
215             sub printTransactionDates {
216 1     1 1 2 my $self = shift;
217 1         2 my $transactionDates = $self->transactionDates();
218 1         1 print join(', ', map { $_->to_string() } @$transactionDates), "\n";
  5         159  
219 1         8 return 1;
220             }
221              
222             sub dateLimitPortion {
223 1500     1500 0 1351 my ($self, $divestment, $acquisition) = @_;
224 1500         1573 my $dateLimit = $self->{dateLimit};
225 1500 100 66     17580 if (!$dateLimit->{start} or !$dateLimit->{end}) {
226 332         390 return 1;
227             }
228             else {
229 1168         1282 my $limitStart = $dateLimit->{start};
230 1168         941 my $limitEnd = $dateLimit->{end};
231 1168         2410 my $realStart = $acquisition->tm();
232 1168         2038 my $realEnd = $divestment->tm();
233 1168 100 100     4820 my $startWithinLimit = ($realStart <= $limitEnd and $realStart >= $limitStart) ? 1 : 0;
234 1168 100 100     3751 my $endWithinLimit = ($realEnd <= $limitEnd and $realEnd >= $limitStart) ? 1 : 0;
235 1168 100 100     5523 if ($startWithinLimit and $endWithinLimit) {
    100 100        
    100 100        
    100          
    50          
236 104         155 return 1;
237             }
238             elsif ($realStart >= $limitEnd or $realEnd <= $limitStart) {
239 862         1257 return 0;
240             }
241             elsif (!$startWithinLimit and !$endWithinLimit) {
242 40         107 my $limitRangeSeconds = $limitEnd->epoch() - $limitStart->epoch();
243 40         84 my $realRangeSeconds = $realEnd->epoch() - $realStart->epoch();
244 40         74 return $limitRangeSeconds / $realRangeSeconds;
245             }
246             elsif ($startWithinLimit) {
247 82         242 my $realRangeSeconds = $realEnd->epoch() - $realStart->epoch();
248 82         153 my $secondsWithinLimit = $limitEnd->epoch() - $realStart->epoch();
249 82         153 return $secondsWithinLimit / $realRangeSeconds;
250             }
251             elsif ($endWithinLimit) {
252 80         219 my $realRangeSeconds = $realEnd->epoch() - $realStart->epoch();
253 80         154 my $secondsWithinLimit = $realEnd->epoch() - $limitStart->epoch();
254 80         159 return $secondsWithinLimit / $realRangeSeconds;
255             }
256             else {
257 0         0 warn "Unexpected result from date comparisons when trying to calculate portion of realization within the given date limit.";
258 0         0 return 0;
259             }
260             }
261             }
262              
263             sub accountPriorPurchase {
264 1004     1004 0 1000 my ($self, $index) = @_;
265 1004 50       1555 if (!$self->{dateSort}) {
266 0         0 confess "Cannot account prior purchase when transactions have not been sorted by date.\n";
267             }
268 1004         962 my $accountTransactions = $self->{accountTransactions};
269 1004         947 my $divestment = $accountTransactions->[$index];
270 1004         1962 my $actionString = $divestment->actionString();
271 1004         2077 my $realization = Finance::StockAccount::Realization->new({
272             stock => $divestment->stock(),
273             divestment => $divestment,
274             });
275            
276 1004         2246 my @priorPurchases = sort { $self->cmpPrice($a, $b) } grep { $_->possiblePurchase($actionString) } @{$accountTransactions}[0 .. $index];
  2603         2444  
  9473         15312  
  1004         1666  
277 1004         1490 foreach my $priorPurchase (@priorPurchases) {
278 1894         3318 my $sharesDivested = $divestment->available();
279 1894 100       3387 last unless $sharesDivested;
280 1500         2502 my $accounted = $priorPurchase->accountShares($sharesDivested);
281 1500 50       2210 if ($accounted) {
282 1500         3551 my $acquisition = Finance::StockAccount::Acquisition->new($priorPurchase, $accounted);
283 1500         2637 my $dateLimitPortion = $self->dateLimitPortion($divestment, $acquisition);
284 1500         3572 $realization->addAcquisition($acquisition, $dateLimitPortion);
285 1500         3083 $divestment->accountShares($accounted);
286             }
287             }
288              
289 1004 100 66     1930 if ($realization->acquisitionCount() and ($realization->costBasis() or $realization->revenue())) {
      66        
290 387         318 push(@{$self->{realizations}}, $realization);
  387         507  
291 387         745 $self->startDate($realization->startDate());
292 387         929 $self->endDate($realization->endDate());
293 387         511 my $stats = $self->{stats};
294 387         882 $stats->{profit} += $realization->realized();
295 387         861 $stats->{totalOutlays} += $realization->costBasis();
296 387         715 $stats->{totalRevenues} += $realization->revenue();
297 387         788 $stats->{commissions} += $realization->commissions();
298 387         711 $stats->{regulatoryFees} += $realization->regulatoryFees();
299 387         720 $stats->{otherFees} += $realization->otherFees();
300 387         384 $stats->{success} = 1;
301 387         1462 return 1;
302             }
303             else {
304 617         1161 my $symbol = $divestment->symbol();
305 617 50       1115 $self->{verbose} and print "[Info] Unable to account for sold shares of symbol $symbol at index $index. There is no acquisition that matches this divestment.\n";
306 617         3438 return 0;
307             }
308             }
309              
310             sub accountSales {
311 446     446 1 396 my $self = shift;
312 446         611 $self->clearPastAccounting();
313 446 100       858 if (!$self->{dateSort}) {
314 44         79 $self->dateSort();
315             }
316 446         470 my $accountTransactions = $self->{accountTransactions};
317 446         427 my $status = 0;
318 446         759 for (my $x=0; $x
319 2612         2252 my $at = $accountTransactions->[$x];
320 2612 100 66     4184 if ($at->sell() or $at->short()) {
321 1004 50       1893 if ($at->available()) {
322 1004         1367 $status += $self->accountPriorPurchase($x);
323             }
324             }
325             }
326 446         702 $self->stale(0);
327 446         789 return $status;
328             }
329              
330             sub startDate {
331 460     460 1 460 my ($self, $tm) = @_;
332 460         568 my $startDate = $self->{stats}{startDate};
333 460 100       2891 if ($tm) {
334 387 100       1280 if (!$startDate) {
    100          
335 179         228 $self->{stats}{startDate} = $tm;
336 179         209 return 1;
337             }
338             elsif ($tm < $startDate) {
339 57         80 $self->{stats}{startDate} = $tm;
340 57         78 return 1;
341             }
342             else {
343 151         196 return 0;
344             }
345             }
346             else {
347 73         129 return $startDate;
348             }
349             }
350              
351             sub endDate {
352 460     460 1 405 my ($self, $tm) = @_;
353 460         520 my $endDate = $self->{stats}{endDate};
354 460 100       2228 if ($tm) {
355 387 100       1154 if (!$endDate) {
    100          
356 179         205 $self->{stats}{endDate} = $tm;
357 179         205 return 1;
358             }
359             elsif ($tm > $endDate) {
360 181         226 $self->{stats}{endDate} = $tm;
361 181         194 return 1;
362             }
363             else {
364 27         38 return 0;
365             }
366             }
367             else {
368 73         139 return $endDate;
369             }
370             }
371              
372             sub checkStats {
373 1303     1303 1 1005 my $self = shift;
374 1303 100       2126 if ($self->{stats}{stale}) {
375 13         17 $self->accountSales();
376             }
377 1303         1049 return 1;
378             }
379              
380             sub profitOverOutlays {
381 2     2 1 3 my $self = shift;
382 2         4 $self->checkStats();
383 2         3 my $stats = $self->{stats};
384 2         20 return $stats->{profit} / $stats->{totalOutlays};
385             }
386              
387             sub profit {
388 185     185 1 175 my $self = shift;
389 185         251 $self->checkStats();
390 185         389 return $self->{stats}{profit};
391             }
392              
393             sub totalOutlays {
394 172     172 1 195 my $self = shift;
395 172         253 $self->checkStats();
396 172         329 return $self->{stats}{totalOutlays};
397             }
398              
399             sub totalRevenues {
400 172     172 1 166 my $self = shift;
401 172         221 $self->checkStats();
402 172         344 return $self->{stats}{totalRevenues};
403             }
404              
405             sub commissions {
406 170     170 1 171 my $self = shift;
407 170         217 $self->checkStats();
408 170         299 return $self->{stats}{commissions};
409             }
410              
411             sub regulatoryFees {
412 170     170 1 152 my $self = shift;
413 170         206 $self->checkStats();
414 170         290 return $self->{stats}{regulatoryFees};
415             }
416              
417             sub otherFees {
418 170     170 1 155 my $self = shift;
419 170         226 $self->checkStats();
420 170         312 return $self->{stats}{otherFees};
421             }
422              
423             sub success {
424 72     72 1 69 my $self = shift;
425 72         111 $self->checkStats();
426 72         189 return $self->{stats}{success};
427             }
428              
429             sub realizations {
430 190     190 1 174 my $self = shift;
431 190         345 $self->checkStats();
432 190         408 return $self->{realizations};
433             }
434              
435             sub realizationsString {
436 20     20 1 18 my $self = shift;
437 20         19 my $string;
438 20         17 foreach my $realization (@{$self->realizations()}) {
  20         32  
439 53         98 $string .= '='x94 . "\n" . $realization->string() . "\n";
440             }
441 20         113 return $string;
442             }
443              
444             sub oneLinerSpacer {
445 1     1 0 207 return '-'x80 . "\n";
446             }
447              
448             sub oneLinerHeader {
449 1     1 0 1 my $self = shift;
450 1         3 return sprintf("%-6s %7s %12s %12s %39s\n", qw(Symbol ROI Outlays Revenues Profit));
451             }
452              
453             sub oneLiner {
454 1     1 0 2 my $self = shift;
455 1         2 return sprintf("%-6s %7.2f %12.2f %12.2f %39.2f\n", $self->symbol(), $self->profitOverOutlays(), $self->totalOutlays(), $self->totalRevenues(), $self->profit());
456             }
457              
458              
459             1;
460              
461              
462             __END__