diff --git a/app/Console/Commands/SyncDailyChange.php b/app/Console/Commands/SyncDailyChange.php index 58aee77..34c2f21 100644 --- a/app/Console/Commands/SyncDailyChange.php +++ b/app/Console/Commands/SyncDailyChange.php @@ -64,9 +64,11 @@ class SyncDailyChange extends Command implements PromptsForMissingInput $portfolio = Portfolio::findOrFail($this->argument('portfolio_id')); + $this->output->write('Syncing daily change history... This may take a moment.', false); + $portfolio->syncDailyChanges(); - $this->line('Awesome! Daily change history for '. $portfolio->title .' has been completed.'); + $this->output->write('Awesome! Daily change history for '. $portfolio->title .' has been completed.', false); } catch (\Throwable $e) { diff --git a/app/Models/Holding.php b/app/Models/Holding.php index 93214ff..8bc0731 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -8,7 +8,6 @@ use App\Models\Portfolio; use App\Models\MarketData; use App\Models\Transaction; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Log; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -255,7 +254,18 @@ class Holding extends Model COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0) AS `owned` "), - DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'BUY' THEN (quantity * cost_basis) ELSE 0 END), 0) AS `cost_basis`"), + DB::raw(" + COALESCE(CASE + WHEN ( + COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - + COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0) + ) = 0 THEN 0 + ELSE SUM(CASE + WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis + ELSE 0 + END) + END, 0) AS cost_basis + "), DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`") ]) ->leftJoin('transactions', function ($join) { diff --git a/app/Models/Portfolio.php b/app/Models/Portfolio.php index 70beb99..17bab25 100644 --- a/app/Models/Portfolio.php +++ b/app/Models/Portfolio.php @@ -101,41 +101,44 @@ class Portfolio extends Model ->select('holdings.symbol', 'holdings.portfolio_id', DB::raw('min(transactions.date) as first_transaction_date')) // get first transaction date ->groupBy(['holdings.symbol', 'holdings.portfolio_id']) ->get(); + + $dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get(); $total_performance = []; - $holdings->each(function($holding) use (&$total_performance) { + $holdings->each(function($holding) use (&$total_performance, $dividends) { + + $holding->setRelation('dividends', $dividends->where('symbol', $holding->symbol)); $all_history = app(MarketDataInterface::class)->history($holding->symbol, $holding->first_transaction_date, now()); - $quantity = $holding->dailyPerformance($holding->first_transaction_date, now()); + $daily_performance = $holding->dailyPerformance($holding->first_transaction_date, now()); $dividends = $holding->dividends->keyBy(function ($dividend, $key) { return $dividend['date']->format('Y-m-d'); - })->toArray(); + }); $dividends_earned = 0; - $daily_performance = []; + $daily = []; - $all_history->sortBy('date')->each(function ($history, $date) use ($quantity, $dividends, &$daily_performance, &$dividends_earned) { + $all_history->sortBy('date')->each(function ($history, $date) use ($daily_performance, $dividends, &$daily, &$dividends_earned) { $close = Arr::get($history, 'close', 0); - $total_market_value = $quantity->get($date)->owned * $close; - $dividend = Arr::get($dividends, $date, []); - $dividends_earned += $quantity->get($date)->owned * Arr::get($dividend, 'dividend_amount', 0); + $total_market_value = $daily_performance->get($date)->owned * $close; + $dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0); - $daily_performance[$date] = [ + $daily[$date] = [ 'date' => $date, 'portfolio_id' => $this->id, 'total_market_value' => $total_market_value, - 'total_cost_basis' => $quantity->get($date)->cost_basis, - 'total_gain' => $total_market_value - $quantity->get($date)->cost_basis, - 'realized_gains' => $quantity->get($date)->realized_gains, + 'total_cost_basis' => $daily_performance->get($date)->cost_basis, + 'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis, + 'realized_gains' => $daily_performance->get($date)->realized_gains, 'total_dividends_earned' => $dividends_earned ]; }); - foreach ($daily_performance as $date => $performance) { + foreach ($daily as $date => $performance) { if (!isset($total_performance[$date])) { $total_performance[$date] = $performance; diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php index 7a5ea48..1cf8c49 100644 --- a/database/factories/TransactionFactory.php +++ b/database/factories/TransactionFactory.php @@ -43,6 +43,13 @@ class TransactionFactory extends Factory ]); } + public function lastYear(): static + { + return $this->state(fn (array $attributes) => [ + 'date' => now()->subYear()->format('Y-m-d'), + ]); + } + public function lastMonth(): static { return $this->state(fn (array $attributes) => [ @@ -50,6 +57,13 @@ class TransactionFactory extends Factory ]); } + public function recent(): static + { + return $this->state(fn (array $attributes) => [ + 'date' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d'), + ]); + } + public function symbol($symbol): static { return $this->state(fn (array $attributes) => [ diff --git a/tests/SyncDailyChangeTest.php b/tests/SyncDailyChangeTest.php index 3379fc7..29ee758 100644 --- a/tests/SyncDailyChangeTest.php +++ b/tests/SyncDailyChangeTest.php @@ -38,6 +38,34 @@ class SyncDailyChangeTest extends TestCase $this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans); } + /** + */ + public function test_cost_basis_is_synced(): void + { + $this->actingAs($user = User::factory()->create()); + + $portfolio = Portfolio::factory()->create(); + + $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(); + + $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(); + + $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(); + + $this->assertEquals(0, $daily_change->total_cost_basis); + } + /** */ public function test_sales_are_captured_as_realized_gains(): void