File Coverage

blib/lib/Finance/StockAccount.pm
Criterion Covered Total %
statement 326 437 74.6
branch 72 112 64.2
condition 27 45 60.0
subroutine 41 52 78.8
pod 28 45 62.2
total 494 691 71.4


line stmt bran cond sub pod time code
1             package Finance::StockAccount;
2              
3             our $VERSION = '0.01';
4              
5 5     5   39706 use strict;
  5         8  
  5         162  
6 5     5   19 use warnings;
  5         6  
  5         107  
7              
8 5     5   16 use Carp;
  5         19  
  5         273  
9 5     5   2340 use POSIX;
  5         24739  
  5         35  
10              
11 5     5   13717 use Finance::StockAccount::Set;
  5         13  
  5         139  
12 5     5   29 use Finance::StockAccount::AccountTransaction;
  5         5  
  5         67  
13 5     5   16 use Finance::StockAccount::Stock;
  5         7  
  5         17378  
14              
15              
16              
17             ### Class definition
18             sub new {
19 16     16 1 2770 my ($class, $options) = @_;
20 16         49 my $self = {
21             sets => {},
22             skipStocks => {},
23             stats => $class->getNewStatsHash(),
24             allowZeroPrice => 0,
25             verbose => 1,
26             };
27 16 50 66     79 if ($options and exists($options->{allowZeroPrice}) and $options->{allowZeroPrice}) {
      66        
28 1         3 $self->{allowZeroPrice} = 1;
29             }
30 16         72 return bless($self, $class);
31             }
32              
33             sub getNewStatsHash {
34             return {
35 35     35 0 432 stale => 1,
36             startDate => undef,
37             endDate => undef,
38             maxCashInvested => 0,
39             totalOutlays => 0,
40             totalRevenues => 0,
41             profit => 0,
42             profitOverOutlays => 0,
43             profitOverMaxCashInvested => 0,
44             profitOverYears => 0,
45             pomciOverYears => 0,
46             commissions => 0,
47             regulatoryFees => 0,
48             otherFees => 0,
49             numberOfTrades => 0,
50             numberExcluded => 0,
51             annualRatio => 0,
52             annualStats => undef,
53             annualStatsStale => 1,
54             quarterlyStats => undef,
55             quarterlyStatsStale => 1,
56             monthlyStats => undef,
57             monthlyStatsStale => 1,
58             };
59             }
60              
61              
62             ### For convenience text output
63             my $statsKeyHeadings = [qw(
64             Outlays Revenues MaxInvested Profit OverOut
65             OverInvested Commiss RegFees OthFees NumTrades
66             )];
67             my $statsKeyHeadingsPattern = "%12s %12s %12s %12s %7s %12s %9s %7s %7s %9s";
68              
69             my $statsKeys = [qw(
70             totalOutlays totalRevenues maxCashInvested profit profitOverOutlays
71             profitOverMaxCashInvested commissions regulatoryFees otherFees numberOfTrades
72             )];
73             my $statsKeysPattern = "%12.2f %12.2f %12.2f %12.2f %7.2f %12.2f %9.2f %7.2f %7.2f %9d";
74              
75             my $statsLinesArray = [
76             'First Trade Date' => 'startDate' => '%35s',
77             'Last Trade Date' => 'endDate' => '%35s',
78             'Maximum Cash Invested at Once' => 'maxCashInvested' => '%35.2f',
79             'Sum Outlays' => 'totalOutlays' => '%35.2f',
80             'Sum Revenues' => 'totalRevenues' => '%35.2f',
81             'Total Profit' => 'profit' => '%35.2f',
82             'Profit Over Years' => 'profitOverYears' => '%35.2f',
83             'Profit Over Sum Outlays' => 'profitOverOutlays' => '%35.2f',
84             'Profit Over Max Cash Invested' => 'profitOverMaxCashInvested' => '%35.2f',
85             'The Above (^) Over Years' => 'pomciOverYears' => '%35.2f',
86             'Total Commissions' => 'commissions' => '%35.2f',
87             'Total Regulatory Fees' => 'regulatoryFees' => '%35.2f',
88             'Total Other Fees' => 'otherFees' => '%35.2f',
89             'Num Trades Included in Stats' => 'numberOfTrades' => '%35d',
90             'Num Trades Excluded from Stats' => 'numberExcluded' => '%35d',
91             ];
92              
93              
94             ### Methods
95             sub stale {
96 281     281 0 255 my ($self, $assertion) = @_;
97 281         297 my $stats = $self->{stats};
98 281 50       338 if (defined($assertion)) {
99 281 50 66     570 if ($assertion == 1 or $assertion == 0) {
100 281 100       328 if ($assertion) {
101 262         202 my $value = 1;
102 262         259 $stats->{stale} = $value;
103 262         227 $stats->{annualStatsStale} = $value;
104 262         226 $stats->{quarterlyStatsStale} = $value;
105 262         253 $stats->{monthlyStatsStale} = $value;
106             }
107             else {
108 19         28 $stats->{stale} = 0;
109             }
110 281         277 return 1;
111             }
112             else {
113 0         0 croak "Method 'stale' only accepts assertions in the form of 1 or 0 -- $assertion is not valid.";
114             }
115             }
116             else {
117 0         0 return $stats->{stale};
118             }
119             }
120              
121             sub allowZeroPrice {
122 3     3 1 5 my ($self, $allowZeroPrice) = @_;
123 3 100       10 if (defined($allowZeroPrice)) {
124 1 50       5 $self->{allowZeroPrice} = $allowZeroPrice ? 1 : 0;
125 1         5 return 1;
126             }
127             else {
128 2         8 return $self->{allowZeroPrice};
129             }
130             }
131              
132             sub getSet {
133 949     949 1 975 my ($self, $hashKey) = @_;
134 949 100       1499 if (exists($self->{sets}{$hashKey})) {
135 907         1280 return $self->{sets}{$hashKey};
136             }
137             else {
138 42         54 return undef;
139             }
140             }
141              
142             sub getSetFiltered {
143 463     463 1 494 my ($self, $hashKey) = @_;
144 463 100       1165 if ($self->{skipStocks}{$hashKey}) {
    50          
145 3         4 return undef;
146             }
147             elsif (exists($self->{sets}{$hashKey})) {
148 460         494 my $set = $self->{sets}{$hashKey};
149 460 100       767 if ($set->stale()) {
150 431         730 $set->accountSales();
151             }
152 460 100       882 if ($set->realizationCount() > 0) {
153 189         290 return $set;
154             }
155             else {
156 271         435 return undef;
157             }
158             }
159             else {
160 0         0 return undef;
161             }
162             }
163              
164             sub getFilteredSets {
165 1     1 1 2 my $self = shift;
166 1         2 my @sets;
167 1         1 foreach my $hashKey (sort keys %{$self->{sets}}) {
  1         15  
168 26         29 my $set = $self->getSetFiltered($hashKey);
169 26 100       50 $set and push(@sets, $set);
170             }
171 1         4 return \@sets;
172             }
173              
174             sub addToSet {
175 257     257 0 234 my ($self, $at) = @_;
176 257 50 33     849 if (ref($at) and ref($at) eq 'Finance::StockAccount::AccountTransaction') {
177 257         491 my $hashKey = $at->hashKey();
178 257         387 my $set = $self->getSet($hashKey);
179 257         219 my $status;
180 257 100       315 if ($set) {
181 215         536 $status = $set->add([$at]);
182             }
183             else {
184 42         149 $set = Finance::StockAccount::Set->new([$at]);
185 42         95 $self->{sets}{$hashKey} = $set;
186 42         37 $status = $set;
187             }
188 257 50       604 $status and $self->stale(1);
189 257         686 return $status;
190             }
191             else {
192 0         0 warn "Method addToSet requires a valid AccountTransaction object.\n";
193 0         0 return 0;
194             }
195             }
196              
197             sub addAccountTransactions {
198 1     1 0 2 my ($self, $accountTransactions) = @_;
199 1 50 33     7 if (ref($accountTransactions) and ref($accountTransactions) eq 'ARRAY') {
200 1         1 my $added = 0;
201 1         2 foreach my $at (@$accountTransactions) {
202 1 50       9 $self->addToSet($at) and $added++;
203             }
204 1 50       3 $added and $self->stale(1);
205 1         2 return $added;
206             }
207             else {
208 0         0 warn "Method addAccountTransactions requiers a reference to an array of AccountTransaction objects.\n";
209 0         0 return 0;
210             }
211             }
212              
213             sub stockTransaction {
214 25     25 1 76 my ($self, $init) = @_;
215 25         28 my $helpString = q{ Please see 'perldoc Finance::StockAccount'.};
216 25 100 100     247 if (!(exists($init->{tm}) or exists($init->{dateString}))) {
    100 66        
    100 66        
    100 66        
    100 66        
    100 100        
217 1         76 carp "A date is required to add a stock transaction, using either the 'tm' key or the 'dateString' key.$helpString";
218 1         54 return 0;
219             }
220             elsif (!(exists($init->{stock}) or exists($init->{symbol}))) {
221 1         69 carp "A stock is required to add a transaction, using either the 'stock' key or the 'symbol' key.$helpString";
222 1         41 return 0;
223             }
224             elsif (!(exists($init->{action}) and (grep { $_ eq $init->{action} } qw(buy sell short cover)))) {
225 1         78 carp "To add a transaction, you must specify an action, one of 'buy', 'sell', 'short', or 'cover'.$helpString";
226 1         72 return 0;
227             }
228             elsif (!(exists($init->{quantity}) and $init->{quantity} =~ /[0-9]/ and $init->{quantity} > 0)) {
229 1         107 carp "To add a transaction, you must specify a numeric quantity, and it must be greater than zero.$helpString";
230 1         70 return 0;
231             }
232             elsif (!(exists($init->{price}) and $init->{price} =~ /[0-9]/)) {
233 1         103 carp "To add a transaction, you must specify a price, and it must be numeric.$helpString";
234 1         64 return 0;
235             }
236             elsif ($init->{price} <= 0 and !$self->{allowZeroPrice}) {
237 2         209 carp "To add a transaction, price must be greater than zero. By default, prices zero or lower are treated as suspect and not included in calculations.\nYou may override this default behavior by setting 'allowZeroPrice' to true.$helpString";
238 2         133 return 0;
239             }
240             else {
241 18 0 0     63 if (exists($init->{stock}) and (exists($init->{symbol}) or exists($init->{exchange}))) {
      33        
242 0         0 carp "To add a transaction, you may specify a Finance::StockAccount::Stock object, or a symbol (optionally with an exchange).\nYou need not specify both. Proceeding on the assumption that the stock object is intentional and the symbol/exchange are accidental...";
243 0 0       0 exists($init->{symbol}) and delete($init->{symbol});
244 0 0       0 exists($init->{exchange}) and delete($init->{exchange});
245             }
246 18         72 my $at = Finance::StockAccount::AccountTransaction->new($init);
247 18 50       30 if ($at) {
248 18         39 return $self->addToSet($at);
249             }
250             else {
251 0         0 carp "Unable to create AccountTransaction object with those parameters.\n";
252 0         0 return 0;
253             }
254             }
255             }
256              
257             sub skipStocks {
258 5     5 1 8 my ($self, @skipStocks) = @_;
259 5         5 my $count = 0;
260 5 100       9 if (scalar(@skipStocks)) {
261 3         6 foreach my $stock (@skipStocks) {
262 4         8 $self->{skipStocks}{$stock} = 1;
263             }
264 3         7 $self->stale(1);
265 3         8 return 1;
266             }
267             else {
268 2         2 return sort keys %{$self->{skipStocks}};
  2         13  
269             }
270             }
271              
272             sub resetSkipStocks {
273 1     1 1 2 my $self = shift;
274 1 50       1 if (scalar(keys %{$self->{skipStocks}})) {
  1         4  
275 1         2 $self->{skipStocks} = {};
276 1         2 $self->stale(1);
277             }
278 1         3 return 1;
279             }
280              
281             sub staleSets {
282 35     35 0 34 my $self = shift;
283 35         35 my $stale = 0;
284 35         31 foreach my $hashKey (keys %{$self->{sets}}) {
  35         125  
285 327         370 my $set = $self->getSet($hashKey);
286 327 50       500 if ($set) {
287 327         507 $stale += $set->stale();
288             }
289             }
290 35         120 return $stale;
291             }
292              
293             sub calculateMaxCashInvested {
294 28     28 0 36 my ($self, $realizations) = @_;
295 28         63 my @allTransactions = sort { $a->tm() <=> $b->tm() } map { @{$_->acquisitions()}, $_->divestment() } @$realizations;
  4575         7013  
  378         275  
  378         615  
296 28         74 my $numberOfTrades = scalar(@allTransactions);
297 28         58 @$realizations = ();
298 28         47 my ($total, $max) = (0, 0);
299 28         106 for (my $x=0; $x
300 1023         765 my $transaction = $allTransactions[$x];
301 1023         1535 $total += 0 - $transaction->cashEffect();
302 1023 100       2418 $total > $max and $max = $total;
303             }
304 28         69 @allTransactions = ();
305 28 50       53 if (wantarray()) {
306 0         0 return $max, $numberOfTrades;
307             }
308             else {
309 28         55 return $max;
310             }
311             }
312              
313             sub calculateStats {
314 19     19 0 21 my $self = shift;
315 19         37 my ($totalOutlays, $totalRevenues, $profit, $commissions, $regulatoryFees, $otherFees, $numberExcluded, $transactionCount) = (0, 0, 0, 0, 0, 0, 0, 0);
316 19         18 my ($startDate, $endDate);
317 19         18 my $setCount = 0;
318 19         27 my @allRealizations = ();
319 19         41 my $stats = $self->getNewStatsHash();
320 19         30 $self->{stats} = $stats;
321 19         77 $self->stale(0);
322 19         14 foreach my $hashKey (keys %{$self->{sets}}) {
  19         70  
323 99         172 my $set = $self->getSetFiltered($hashKey);
324 99 100       157 if ($set) {
325 72 50       123 if ($set->stale()) {
326 0         0 $set->accountSales();
327             }
328 72         186 $numberExcluded += $set->unrealizedTransactionCount();
329 72 50       154 next unless $set->success();
330              
331             ### Simple Totals
332 72         149 $totalOutlays += $set->totalOutlays();
333 72         182 $totalRevenues += $set->totalRevenues();
334 72         130 $profit += $set->profit();
335 72         152 $commissions += $set->commissions();
336 72         135 $regulatoryFees += $set->regulatoryFees();
337 72         154 $otherFees += $set->otherFees();
338 72         141 $transactionCount += $set->transactionCount();
339              
340             ### Date-Aware Totals
341 72         74 push(@allRealizations, @{$set->realizations()});
  72         136  
342              
343             ### End-Date, Start-Date
344 72         137 my $setStart = $set->startDate();
345 72 50       593 $setStart or die "Didn't get set startDate for hashkey $hashKey\n";
346 72 100       238 if (!defined($startDate)) {
    100          
347 15         18 $startDate = $setStart;
348             }
349             elsif ($setStart < $startDate) {
350 12         16 $startDate = $setStart;
351             }
352 72         150 my $setEnd = $set->endDate();
353 72 100       340 if (!$endDate) {
    100          
354 15         20 $endDate = $setEnd;
355             }
356             elsif ($setEnd > $endDate) {
357 10         17 $endDate = $setEnd;
358             }
359 72         116 $setCount++;
360             }
361             else {
362 27         54 my $unfilteredSet = $self->getSet($hashKey);
363 27         62 $numberExcluded += $unfilteredSet->unrealizedTransactionCount();
364 27 100       76 if ($self->{skipStocks}{$hashKey}) {
365 3         5 $numberExcluded += $unfilteredSet->transactionCount();
366             }
367             }
368             }
369 19         45 $stats->{numberExcluded} = $numberExcluded;
370 19 100       45 if ($setCount > 0) {
371 15 50       28 if ($totalOutlays) {
372 15         22 my $profitOverOutlays = $profit / $totalOutlays;
373 15         22 $stats->{totalOutlays} = $totalOutlays;
374 15         18 $stats->{totalRevenues} = $totalRevenues;
375 15         22 $stats->{profit} = $profit;
376 15         19 $stats->{commissions} = $commissions;
377 15         17 $stats->{regulatoryFees} = $regulatoryFees;
378 15         18 $stats->{otherFees} = $otherFees;
379 15         18 $stats->{profitOverOutlays} = $profitOverOutlays;
380 15         15 $stats->{startDate} = $startDate;
381 15         17 $stats->{endDate} = $endDate;
382 15         20 my $secondsInYear = 60 * 60 * 24 * 365.25;
383 15         55 my $secondsInAccount = $endDate->epoch() - $startDate->epoch();
384 15 50       34 if (!$secondsInAccount) {
385 0         0 croak "No time passed in account? Can't calculate time-related stats.";
386             }
387 15         21 my $annualRatio = $secondsInYear / $secondsInAccount;
388 15         24 $stats->{annualRatio} = $annualRatio;
389 15         26 $stats->{profitOverYears} = $profit * $annualRatio;
390 15         42 my $maxCashInvested = $self->calculateMaxCashInvested(\@allRealizations);
391 15         24 $stats->{maxCashInvested} = $maxCashInvested;
392 15         27 $stats->{profitOverOutlays} = $profit / $totalOutlays;
393 15         19 my $pomci = $profit / $maxCashInvested;
394 15         18 $stats->{profitOverMaxCashInvested} = $pomci;
395 15         19 $stats->{pomciOverYears} = $pomci * $annualRatio;
396 15         16 $stats->{numberOfTrades} = $transactionCount;
397 15         38 return 1;
398             }
399             else {
400 0         0 carp "No totalOutlays found on which to compute stats.\n";
401 0         0 return 0;
402             }
403             }
404             else {
405 4         537 carp "No realized gains in stock account.\n";
406 4         280 return 0;
407             }
408             }
409              
410             sub getStats {
411 52     52 0 62 my $self = shift;
412 52 100 100     186 if ($self->{stats}{stale} or $self->staleSets()) {
413 19         55 return $self->calculateStats();
414             }
415             else {
416 33         33 return 1;
417             }
418             }
419              
420             sub stats {
421 0     0 1 0 my $self = shift;
422 0         0 $self->getStats();
423 0         0 my $stats = $self->{stats};
424 0         0 my @stats;
425 0         0 for (my $x=0; $x
426 0         0 my $key = $statsLinesArray->[$x+1];
427 0         0 push(@stats, $key, $stats->{$key});
428             }
429 0         0 return \@stats;
430             }
431              
432             sub statsString {
433 1     1 1 3 my $self = shift;
434 1         4 $self->getStats();
435 1         2 my $stats = $self->{stats};
436 1         2 my $statsString;
437 1         6 for (my $x=0; $x
438 15         25 my ($name, $key, $valPattern) = @$statsLinesArray[$x .. $x+2];
439 15         91 $statsString .= sprintf("%30s $valPattern\n", $name, $stats->{$key});
440             }
441 1         7 return $statsString;
442             }
443              
444             sub availableAcquisitionsString {
445 0     0 1 0 my ($self, $inputKey) = @_;
446 0         0 my ($string, @hashKeys);
447 0 0       0 if ($inputKey) {
448 0         0 @hashKeys = ($inputKey);
449             }
450             else {
451 0         0 @hashKeys = keys %{$self->{sets}};
  0         0  
452             }
453 0         0 foreach my $hashKey (@hashKeys) {
454 0         0 my $set = $self->getSet($hashKey);
455 0 0       0 if ($set) {
456 0         0 $string .= $set->availableAcquisitionsString();
457             }
458             }
459 0         0 return $string;
460             }
461              
462             sub statsForPeriod {
463 13     13 1 19 my ($self, $tm1, $tm2) = @_;
464 13         35 my ($totalOutlays, $totalRevenues, $profit, $commissions, $regulatoryFees, $otherFees, $transactionCount) = (0, 0, 0, 0, 0, 0, 0);
465 13         14 my @allRealizations = ();
466 13         13 my $setCount = 0;
467 13         15 foreach my $hashKey (keys %{$self->{sets}}) {
  13         86  
468 338         547 my $unfilteredSet = $self->getSet($hashKey);
469 338         713 $unfilteredSet->setDateLimit($tm1, $tm2);
470 338         554 my $set = $self->getSetFiltered($hashKey);
471 338 100       610 if ($set) {
472 98         94 $setCount++;
473 98         224 $totalOutlays += $set->totalOutlays();
474 98         207 $totalRevenues += $set->totalRevenues();
475 98         202 $profit += $set->profit();
476 98         200 $commissions += $set->commissions();
477 98         194 $regulatoryFees += $set->regulatoryFees();
478 98         189 $otherFees += $set->otherFees();
479 98         204 $transactionCount += $set->transactionCount();
480 98         99 push(@allRealizations, @{$set->realizations()});
  98         205  
481             }
482 338         612 $unfilteredSet->clearDateLimit();
483             }
484 13 50 33     89 if ($setCount > 0 and $totalOutlays) {
485 13         58 my $maxCashInvested = $self->calculateMaxCashInvested(\@allRealizations);
486             return {
487 13         135 totalOutlays => $totalOutlays,
488             totalRevenues => $totalRevenues,
489             maxCashInvested => $maxCashInvested,
490             profit => $profit,
491             profitOverOutlays => $profit / $totalOutlays,
492             profitOverMaxCashInvested => $profit / $maxCashInvested,
493             commissions => $commissions,
494             regulatoryFees => $regulatoryFees,
495             otherFees => $otherFees,
496             numberOfTrades => $transactionCount,
497             };
498             }
499             else {
500 0         0 carp "I couldn't calculate stats for the period from $tm1 to $tm2 for this stock account.\n";
501 0         0 return undef;
502             }
503             }
504              
505             sub calculateAnnualStats {
506 1     1 0 1 my $self = shift;
507 1         2 $self->getStats();
508 1         2 my $stats = $self->{stats};
509 1         2 my $startDate = $stats->{startDate};
510 1         2 my $endDate = $stats->{endDate};
511 1         4 my $offset = $startDate->offset();
512 1         1 my $annualStats = [];
513 1         7 foreach my $year ($startDate->year() .. $endDate->year()) {
514 3         25 my $yearStart = Time::Moment->new(
515             year => $year,
516             month => 1,
517             day => 1,
518             hour => 0,
519             minute => 0,
520             second => 01,
521             nanosecond => 0,
522             offset => $offset,
523             );
524 3         16 my $yearEnd = Time::Moment->new(
525             year => $year,
526             month => 12,
527             day => 31,
528             hour => 23,
529             minute => 59,
530             second => 59,
531             nanosecond => 0,
532             offset => $offset,
533             );
534 3         8 my $yearStats = $self->statsForPeriod($yearStart, $yearEnd);
535 3         8 $yearStats->{year} = $year;
536 3         25 push(@$annualStats, $yearStats);
537             }
538 1         3 $stats->{annualStats} = $annualStats;
539 1         2 $stats->{annualStatsStale} = 0;
540 1         8 return $annualStats;
541             }
542              
543             sub calculateQuarterlyStats {
544 1     1 0 2 my $self = shift;
545 1         4 $self->getStats();
546 1         2 my $quarterlyStats = [];
547 1         3 my $stats = $self->{stats};
548 1         2 my $startDate = $stats->{startDate};
549 1         2 my $endDate = $stats->{endDate};
550 1         4 my $offset = $startDate->offset();
551 1         4 my $startYear = $startDate->year();
552 1         18 my $startQuarter = ceil($startDate->month()/3);
553 1         11 my $currDate = Time::Moment->new(
554             year => $startYear,
555             month => ($startQuarter - 1) * 3 + 1,
556             day => 1,
557             hour => 0,
558             minute => 0,
559             second => 01,
560             nanosecond => 0,
561             offset => $offset,
562             );
563 1         5 while ($currDate < $endDate) {
564 10         61 my $quarterEnd = $currDate->plus_months(3);
565 10         27 my $quarterStats = $self->statsForPeriod($currDate, $quarterEnd);
566 10         56 $quarterStats->{year} = $currDate->year();
567 10         73 $quarterStats->{quarter} = ceil($currDate->month()/3);
568 10         41 push(@$quarterlyStats, $quarterStats);
569 10         47 $currDate = $quarterEnd;
570             }
571 1         3 $stats->{quarterlyStats} = $quarterlyStats;
572 1         2 $stats->{quarterlyStatsStale} = 0;
573 1         13 return $quarterlyStats;
574             }
575              
576             sub calculateMonthlyStats {
577 0     0 0 0 my $self = shift;
578 0         0 $self->getStats();
579 0         0 my $monthlyStats = [];
580 0         0 my $stats = $self->{stats};
581 0         0 my $startDate = $stats->{startDate};
582 0         0 my $endDate = $stats->{endDate};
583 0         0 my $offset = $startDate->offset();
584 0         0 my $startYear = $startDate->year();
585 0         0 my $startMonth = $startDate->month();
586 0         0 my $currDate = Time::Moment->new(
587             year => $startYear,
588             month => $startMonth,
589             day => 1,
590             hour => 0,
591             minute => 0,
592             second => 01,
593             nanosecond => 0,
594             offset => $offset,
595             );
596 0         0 while ($currDate < $endDate) {
597 0         0 my $monthEnd = $currDate->plus_months(1);
598 0         0 my $monthStats = $self->statsForPeriod($currDate, $monthEnd);
599 0         0 $monthStats->{year} = $currDate->year();
600 0         0 $monthStats->{month} = $currDate->month();
601 0         0 push(@$monthlyStats, $monthStats);
602 0         0 $currDate = $monthEnd;
603             }
604 0         0 $stats->{monthlyStats} = $monthlyStats;
605 0         0 $stats->{monthlyStatsStale} = 0;
606 0         0 return $monthlyStats;
607             }
608              
609             sub annualStats {
610 1     1 1 2 my $self = shift;
611 1         2 my $stats = $self->{stats};
612 1 50       3 if ($stats->{annualStatsStale}) {
613 1         4 return $self->calculateAnnualStats();
614             }
615             else {
616 0         0 return $stats->{annualStats};
617             }
618             }
619              
620             sub quarterlyStats {
621 1     1 1 3 my $self = shift;
622 1         2 my $stats = $self->{stats};
623 1 50       4 if ($stats->{quarterlyStatsStale}) {
624 1         3 return $self->calculateQuarterlyStats();
625             }
626             else {
627 0         0 return $stats->{quarterlyStats};
628             }
629             }
630              
631             sub monthlyStats {
632 0     0 1 0 my $self = shift;
633 0         0 my $stats = $self->{stats};
634 0 0       0 if ($stats->{monthlyStatsStale}) {
635 0         0 return $self->calculateMonthlyStats();
636             }
637             else {
638 0         0 return $stats->{monthlyStats};
639             }
640             }
641              
642             sub periodicStatsString {
643 0     0 0 0 my ($self, $params) = @_;
644 0         0 my $periodHeadings = $params->{periodHeadings};
645 0         0 my $periodHeadingsPattern = $params->{periodHeadingsPattern};
646 0         0 my $periodKeys = $params->{periodKeys};
647 0         0 my $periodValuesPattern = $params->{periodValuesPattern};
648 0         0 my $periodHeadingsWidth = $params->{periodHeadingsWidth};
649 0         0 my $statsArray = $params->{statsArray};
650 0         0 my $statsString = sprintf("$periodHeadingsPattern $statsKeyHeadingsPattern\n", @$periodHeadings, @$statsKeyHeadings);
651 0         0 my $lineLength = length($statsString);
652 0         0 my $pattern = "$periodValuesPattern $statsKeysPattern\n";
653 0         0 my $totals;
654 0         0 my $verbose = $self->{verbose};
655 0 0       0 if ($verbose) {
656 0         0 $totals = [map { 0 } (0 .. scalar(@$statsKeys) - 1)];
  0         0  
657             }
658 0         0 foreach my $period (@$statsArray) {
659 0         0 my @row = map { $period->{$_} } @$periodKeys;
  0         0  
660 0         0 for (my $x=0; $x
661 0         0 my $key = $statsKeys->[$x];
662 0         0 my $value = $period->{$key};
663 0 0       0 if ($verbose) {
664 0         0 $totals->[$x] += $value;
665             }
666 0         0 push(@row, $value);
667             }
668 0         0 $statsString .= sprintf($pattern, @row);
669             }
670 0 0       0 if ($verbose) {
671 0         0 $statsString .= '-'x$lineLength . "\n"
672             . sprintf("%-${periodHeadingsWidth}s $statsKeysPattern\n", 'COL SUMS', @$totals);
673             }
674 0         0 $statsString .= '-'x$lineLength . "\n"
675 0         0 . sprintf("%-${periodHeadingsWidth}s $statsKeysPattern\n", 'ACCT TOTAL', map { $self->{stats}{$_} } @$statsKeys);
676 0         0 return $statsString;
677             }
678              
679             sub annualStatsString {
680 0     0 1 0 my $self = shift;
681 0         0 return $self->periodicStatsString({
682             periodHeadings => [qw(Year)],
683             periodHeadingsPattern => "%10s",
684             periodKeys => [qw(year)],
685             periodValuesPattern => "%10d",
686             periodHeadingsWidth => 10,
687             statsArray => $self->annualStats(),
688             });
689             }
690              
691             sub quarterlyStatsString {
692 0     0 1 0 my $self = shift;
693 0         0 return $self->periodicStatsString({
694             periodHeadings => [qw(Year Quarter)],
695             periodHeadingsPattern => "%4s %7s",
696             periodKeys => [qw(year quarter)],
697             periodValuesPattern => "%4d %7d",
698             periodHeadingsWidth => 12,
699             statsArray => $self->quarterlyStats(),
700             });
701             }
702              
703             sub monthlyStatsString {
704 0     0 1 0 my $self = shift;
705 0         0 return $self->periodicStatsString({
706             periodHeadings => [qw(Year Month)],
707             periodHeadingsPattern => "%4s %5s",
708             periodKeys => [qw(year month)],
709             periodValuesPattern => "%4d %5d",
710             periodHeadingsWidth => 10,
711             statsArray => $self->monthlyStats(),
712             });
713             }
714              
715             sub profit {
716 14     14 1 30 my $self = shift;
717 14         32 $self->getStats();
718 14         97 return $self->{stats}{profit};
719             }
720              
721             sub maxCashInvested {
722 4     4 1 11 my $self = shift;
723 4         10 $self->getStats();
724 4         21 return $self->{stats}{maxCashInvested};
725             }
726              
727             sub profitOverOutlays {
728 3     3 1 7 my $self = shift;
729 3         6 $self->getStats();
730 3         27 return $self->{stats}{profitOverOutlays};
731             }
732              
733             sub profitOverMaxCashInvested {
734 3     3 0 9 my $self = shift;
735 3         5 $self->getStats();
736 3         23 return $self->{stats}{profitOverMaxCashInvested};
737             }
738              
739             sub profitOverYears {
740 3     3 1 8 my $self = shift;
741 3         6 $self->getStats();
742 3         25 return $self->{stats}{profitOverYears};
743             }
744              
745             sub profitOverMaxCashInvestedOverYears {
746 2     2 0 4 my $self = shift;
747 2         5 $self->getStats();
748 2         20 return $self->{stats}{pomciOverYears};
749             }
750              
751             sub commissions {
752 5     5 1 13 my $self = shift;
753 5         10 $self->getStats();
754 5         28 return $self->{stats}{commissions};
755             }
756              
757             sub totalCommissions {
758 0     0 1 0 return shift->commissions();
759             }
760              
761             sub regulatoryFees {
762 3     3 1 5 my $self = shift;
763 3         7 $self->getStats();
764 3         18 return $self->{stats}{regulatoryFees};
765             }
766              
767             sub otherFees {
768 1     1 1 2 my $self = shift;
769 1         3 $self->getStats();
770 1         6 return $self->{stats}{otherFees};
771             }
772              
773             sub numberOfTrades {
774 5     5 0 11 my $self = shift;
775 5         10 $self->getStats();
776 5         21 return $self->{stats}{numberOfTrades};
777             }
778              
779             sub numberExcluded {
780 6     6 0 11 my $self = shift;
781 6         9 $self->getStats();
782 6         27 return $self->{stats}{numberExcluded};
783             }
784              
785             sub totalFees {
786 0     0 0 0 my $self = shift;
787 0         0 return $self->regulatoryFees() + $self->otherFees();
788             }
789              
790             sub summaryByStock {
791 0     0 1 0 my $self = shift;
792 0         0 my $spacer = Finance::StockAccount::Set->oneLinerSpacer();
793 0         0 my $string = $spacer . Finance::StockAccount::Set->oneLinerHeader() . $spacer;
794 0         0 my $sets = $self->getFilteredSets();
795 0         0 foreach my $set (@$sets) {
796 0         0 $string .= $set->oneLiner() . $spacer;
797             }
798 0         0 return $string;
799             }
800              
801              
802             sub realizationsString {
803 1     1 1 2 my $self = shift;
804 1         5 my $string = Finance::StockAccount::Realization->headerString();
805 1         4 my $sets = $self->getFilteredSets();
806 1         2 foreach my $set (@$sets) {
807 19         41 $string .= $set->realizationsString();
808             }
809 1         30 return $string;
810             }
811              
812              
813             1;
814              
815             __END__