From 7d119a8c24c132d5aa0e49d5bdc5abb44947c486 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Mon, 23 Sep 2024 15:09:35 -0500 Subject: [PATCH] simplify tests and daily change calculations --- app/Models/Holding.php | 38 ++++++++----------- app/Models/Portfolio.php | 20 +++++----- .../views/components/ib-apex-chart.blade.php | 2 +- .../portfolio-performance-chart.blade.php | 28 +++++++------- tests/CaptureDailyChangeTest.php | 4 +- tests/DashboardTest.php | 8 ++-- tests/SyncDailyChangeTest.php | 34 ++++++++++++----- tests/TransactionsTest.php | 8 ++-- 8 files changed, 77 insertions(+), 65 deletions(-) diff --git a/app/Models/Holding.php b/app/Models/Holding.php index 479e9ef..9e22242 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -71,7 +71,7 @@ class Holding extends Model CASE WHEN transaction_type = 'BUY' AND transactions.symbol = dividends.symbol AND transactions.portfolio_id = '$this->portfolio_id' - AND dividends.date >= transactions.date + AND date(dividends.date) >= date(transactions.date) THEN transactions.quantity ELSE 0 END ) AS purchased") @@ -79,10 +79,19 @@ class Holding extends Model CASE WHEN transaction_type = 'SELL' AND transactions.symbol = dividends.symbol AND transactions.portfolio_id = '$this->portfolio_id' - AND dividends.date >= transactions.date + AND date(dividends.date) >= date(transactions.date) THEN transactions.quantity ELSE 0 END ) AS sold") + ->selectRaw('SUM( + (CASE WHEN transaction_type = "BUY" + AND date(transactions.date) <= date(dividends.date) + THEN transactions.quantity ELSE 0 END + - CASE WHEN transaction_type = "SELL" + AND date(transactions.date) <= date(dividends.date) + THEN transactions.quantity ELSE 0 END) + * dividends.dividend_amount + ) AS total_received') ->join('transactions', 'transactions.symbol', 'dividends.symbol') ->groupBy(['dividends.symbol','dividends.date','dividends.dividend_amount']) ->orderBy('dividends.date', 'DESC') @@ -186,32 +195,15 @@ class Holding extends Model ? $query->total_cost_basis / $query->qty_purchases : 0; - // pull dividend data joined with holdings/transactions - $dividends = Dividend::select('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount') - ->selectRaw(' - (COALESCE(CASE WHEN transactions.transaction_type = "BUY" - AND date(transactions.date) <= date(dividends.date) - THEN transactions.quantity ELSE 0 END, 0) - - COALESCE(CASE WHEN transactions.transaction_type = "SELL" - AND date(transactions.date) <= date(dividends.date) - THEN transactions.quantity ELSE 0 END, 0)) - * dividends.dividend_amount - AS total_received - ') - ->join('transactions', 'transactions.symbol', 'dividends.symbol') - ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') - ->where('dividends.symbol', $this->symbol) - ->where('transactions.portfolio_id', $this->portfolio_id) - ->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received') - ->get(); - // update holding $this->fill([ 'quantity' => $total_quantity, 'average_cost_basis' => $average_cost_basis, 'total_cost_basis' => $total_quantity * $average_cost_basis, - 'realized_gain_dollars' => $query->total_sale_price > 0 ? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases)) : 0, - 'dividends_earned' => $dividends->sum('total_received') + 'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0 + ? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases)) + : 0, + 'dividends_earned' => $this->dividends->sum('total_received') ]); $this->save(); diff --git a/app/Models/Portfolio.php b/app/Models/Portfolio.php index a6dd335..f319180 100644 --- a/app/Models/Portfolio.php +++ b/app/Models/Portfolio.php @@ -127,15 +127,17 @@ class Portfolio extends Model $total_market_value = $performance->owned * $close; $dividends_earned += $performance->owned * ($dividends->get($date)?->dividend_amount ?? 0); - $daily[$date] = [ - 'date' => $date, - 'portfolio_id' => $this->id, - 'total_market_value' => $total_market_value, - 'total_cost_basis' => $performance->cost_basis, - 'total_gain' => $total_market_value - $performance->cost_basis, - 'realized_gains' => $performance->realized_gains, - 'total_dividends_earned' => $dividends_earned - ]; + if (Carbon::parse($date)->isWeekday()) { + $daily[$date] = [ + 'date' => $date, + 'portfolio_id' => $this->id, + 'total_market_value' => $total_market_value, + 'total_cost_basis' => $performance->cost_basis, + 'total_gain' => $total_market_value - $performance->cost_basis, + 'realized_gains' => $performance->realized_gains, + 'total_dividends_earned' => $dividends_earned + ]; + } }); foreach ($daily as $date => $performance) { diff --git a/resources/views/components/ib-apex-chart.blade.php b/resources/views/components/ib-apex-chart.blade.php index c44a07f..2d4b0eb 100644 --- a/resources/views/components/ib-apex-chart.blade.php +++ b/resources/views/components/ib-apex-chart.blade.php @@ -17,7 +17,7 @@ 'enabled' => false ] ], - 'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F', ], + 'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F'], 'stroke' => [ 'curve' => "smooth", 'width' => 3 diff --git a/resources/views/livewire/portfolio-performance-chart.blade.php b/resources/views/livewire/portfolio-performance-chart.blade.php index 3bdd279..4e5734f 100644 --- a/resources/views/livewire/portfolio-performance-chart.blade.php +++ b/resources/views/livewire/portfolio-performance-chart.blade.php @@ -11,7 +11,7 @@ new class extends Component { public ?Portfolio $portfolio; public String $name = 'portfolio'; public String $scope = 'YTD'; - public Array $options = [ + public Array $scopeOptions = [ ['id' => '1M', 'name' => '1 month', 'method' => 'subMonths', 'args' => [1]], ['id' => '3M', 'name' => '3 months', 'method' => 'subMonths', 'args' => [3]], ['id' => 'YTD', 'name' => 'Year to date', 'method' => 'startOfYear', 'args' => []], @@ -31,7 +31,7 @@ new class extends Component { public function generatePerformanceData() { - $filterMethod = collect($this->options)->where('id', $this->scope)->first(); + $filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first(); $dailyChangeQuery = DailyChange::query(); @@ -41,14 +41,15 @@ new class extends Component { } else { - $dailyChangeQuery->selectRaw('date, + $dailyChangeQuery->selectRaw(' + date, SUM(total_market_value) as total_market_value, SUM(total_cost_basis) as total_cost_basis, - SUM(total_gain) as total_gain' - )->groupBy('date'); + SUM(total_gain) as total_gain, + --SUM(realized_gains) as realized_gains, + --SUM(total_dividends_earned) as total_dividends_earned + ')->groupBy('date'); - // SUM(total_dividends_earned) as total_dividends_earned, - // SUM(realized_gains) as realized_gains } if ($filterMethod['method']) { @@ -72,10 +73,11 @@ new class extends Component { 'name' => __('Market Gain'), 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_gain])->toArray() ], - [ - 'name' => __('Dividends Earned'), - 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_dividends_earned])->toArray() - ], + + // [ + // 'name' => __('Dividends Earned'), + // 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_dividends_earned])->toArray() + // ], // [ // 'name' => __('Realized Gains'), // 'data' => $dailyChange->map(fn($data) => [$data->date, $data->realized_gains])->toArray() @@ -93,7 +95,7 @@ new class extends Component { public function getScopeName($scope) { - return collect($this->options)->where('id', $scope)->first()['name']; + return collect($this->scopeOptions)->where('id', $scope)->first()['name']; } }; ?> @@ -115,7 +117,7 @@ new class extends Component { - @foreach($options as $option) + @foreach($scopeOptions as $option) actingAs($user = User::factory()->create()); $this->portfolio = Portfolio::factory()->create(); - Transaction::factory(5)->buy()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); - $this->transaction = Transaction::factory()->sell()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); + Transaction::factory(5)->buy()->lastYear()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); + $this->transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); } /** diff --git a/tests/DashboardTest.php b/tests/DashboardTest.php index a5c7434..8a73069 100644 --- a/tests/DashboardTest.php +++ b/tests/DashboardTest.php @@ -55,9 +55,9 @@ class DashboardTest extends TestCase $this->actingAs($user = User::factory()->create()); $portfolio = Portfolio::factory()->create(); - - Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create(); - $transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create(); + + Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); + $transaction = Transaction::factory()->sell()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create(); $metrics = Holding::query() ->myHoldings() @@ -65,7 +65,7 @@ class DashboardTest extends TestCase ->first(); $this->assertEqualsWithDelta( - $transaction->sale_price - $transaction->cost_basis, + $transaction->sale_price - $transaction->cost_basis, $metrics->realized_gain_dollars, 0.01 ); diff --git a/tests/SyncDailyChangeTest.php b/tests/SyncDailyChangeTest.php index 29ee758..95a88a8 100644 --- a/tests/SyncDailyChangeTest.php +++ b/tests/SyncDailyChangeTest.php @@ -5,6 +5,7 @@ namespace Tests; use Tests\TestCase; use App\Models\User; use App\Models\Holding; +use Carbon\CarbonPeriod; use App\Models\Portfolio; use App\Models\DailyChange; use App\Models\Transaction; @@ -33,7 +34,7 @@ class SyncDailyChangeTest extends TestCase $portfolio->syncDailyChanges(); $count_of_daily_changes = $portfolio->daily_change()->count('date'); - $days_between_now_and_first_trans = (int) now()->diffInDays($portfolio->transactions()->min('date'), true) + 1; + $days_between_now_and_first_trans = (int) CarbonPeriod::create($portfolio->transactions()->min('date'), now())->filter('isWeekday')->count(); $this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans); } @@ -49,19 +50,28 @@ class SyncDailyChangeTest extends TestCase $first_transaction = Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create(); Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); $holding = Holding::symbol('ACME')->portfolio($portfolio->id)->first(); - $daily_change = DailyChange::whereDate('date', $first_transaction->date)->first(); + $daily_change = DailyChange::whereDate('date', '<=', $first_transaction->date->addDays(2)) + ->whereDate('date', '>=', $first_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); $this->assertEquals($holding->average_cost_basis, $daily_change->total_cost_basis); $second_transaction = Transaction::factory()->buy()->lastYear()->portfolio($portfolio->id)->symbol('ACME')->create(); Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); - $daily_change = DailyChange::whereDate('date', $second_transaction->date)->first(); + $daily_change = DailyChange::whereDate('date', '<=', $second_transaction->date->addDays(2)) + ->whereDate('date', '>=', $second_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); $this->assertEqualsWithDelta($first_transaction->cost_basis + $second_transaction->cost_basis, $daily_change->total_cost_basis, 0.01); $third_transaction = Transaction::factory(2)->sell()->lastMonth()->portfolio($portfolio->id)->symbol('ACME')->create()->first(); Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]); - $daily_change = DailyChange::whereDate('date', $third_transaction->date)->first(); + $daily_change = DailyChange::whereDate('date', '<=', $third_transaction->date->addDays(2)) + ->whereDate('date', '>=', $third_transaction->date->subDays(2)) + ->orderByDesc('date') + ->first(); $this->assertEquals(0, $daily_change->total_cost_basis); } @@ -99,12 +109,14 @@ class SyncDailyChangeTest extends TestCase $this->assertEquals($day_before->realized_gains, 0); - $day_after = DailyChange::query() + $after = DailyChange::query() ->portfolio($portfolio->id) - ->whereDate('date', $sale_transaction->date->addDays(1)) + ->whereDate('date', '<=', $sale_transaction->date->addDays(2)) + ->whereDate('date', '>=', $sale_transaction->date->subDays(2)) + ->orderByDesc('date') ->first(); - $this->assertEqualsWithDelta($day_after->realized_gains, $realized_gain, 0.01); + $this->assertEqualsWithDelta($after->realized_gains, $realized_gain, 0.01); } public function test_dividends_captured_in_daily_change_sync(): void @@ -124,7 +136,9 @@ class SyncDailyChangeTest extends TestCase $first_dividend_change = DailyChange::query() ->portfolio($portfolio->id) - ->whereDate('date', $dividends->first()->date) + ->whereDate('date', '<=', $dividends->first()->date->addDays(2)) + ->whereDate('date', '>=', $dividends->first()->date->subDays(2)) + ->orderByDesc('date') ->first(); $owned = $dividends->first()->purchased - $dividends->first()->sold; @@ -133,7 +147,9 @@ class SyncDailyChangeTest extends TestCase $last_dividend_change = DailyChange::query() ->portfolio($portfolio->id) - ->whereDate('date', $dividends->last()->date) + ->whereDate('date', '<=', $dividends->last()->date->addDays(2)) + ->whereDate('date', '>=', $dividends->last()->date->subDays(2)) + ->orderByDesc('date') ->first(); $total_dividends = $dividends->reduce(function (?float $carry, $dividend) { diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php index 98fc924..8020499 100644 --- a/tests/TransactionsTest.php +++ b/tests/TransactionsTest.php @@ -30,9 +30,9 @@ class TransactionsTest extends TestCase { $this->actingAs($user = User::factory()->create()); - Transaction::factory(5)->buy()->symbol('AAPL')->create(); + Transaction::factory(5)->buy()->lastYear()->symbol('AAPL')->create(); - $transaction = Transaction::factory()->sell()->symbol('AAPL')->create(); + $transaction = Transaction::factory()->sell()->lastMonth()->symbol('AAPL')->create(); $this->assertNotNull($transaction->cost_basis); } @@ -56,8 +56,8 @@ class TransactionsTest extends TestCase $portfolio = Portfolio::factory()->create(); - Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create(); - $transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create(); + Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); + $transaction = Transaction::factory()->sell()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create(); $this->assertDatabaseHas('holdings', [ 'portfolio_id' => $portfolio->id,