actingAs($user = User::factory()->create()); $this->portfolio = Portfolio::factory()->create(); } public function test_daily_change_for_portfolios() { Transaction::factory(5)->buy()->lastYear()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); $transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); // Run the command Artisan::call('capture:daily-change'); // Assert the daily change was captured for the portfolio $this->assertDatabaseHas('daily_change', [ 'portfolio_id' => $this->portfolio->id, ]); $output = Artisan::output(); $this->assertStringContainsString('Capturing daily change for', $output); $daily_change = DailyChange::where([ 'portfolio_id' => $this->portfolio->id, ])->get(); $this->assertCount(1, $daily_change); $quantity = Holding::where('symbol', 'AAPL')->sum('quantity'); $this->assertEqualsWithDelta( $transaction->market_data->market_value_base * $quantity, $daily_change->first()->total_market_value, 0.01 ); } public function test_can_sync_daily_change_history(): void { // create some transaction history Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('ACME')->create(); Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('ACME')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('GOOG')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('FOO')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('BAR')->create(); // sync $this->portfolio->syncDailyChanges(); // ensure count matches $end_date = now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day'))) ? now()->subDay() : now(); $count_of_daily_changes = $this->portfolio->daily_change()->count('date'); $days_between_now_and_first_trans = (int) CarbonPeriod::create( $this->portfolio->transactions()->min('date'), $end_date )->filter('isWeekday') ->count(); $this->assertEquals($days_between_now_and_first_trans, $count_of_daily_changes); // ensure market value matches $holding_performance = $this->portfolio->holdings()->withPerformance()->get(); $total_market_value = $holding_performance->sum('total_market_value'); $daily_change = $this->portfolio->daily_change()->orderBy('date')->get()->last(); $this->assertEqualsWithDelta($total_market_value, $daily_change->total_market_value, 0.01); } public function test_cost_basis_is_calculated(): void { $first_transaction = Transaction::factory()->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('ACME')->create(); $this->portfolio->syncDailyChanges(); $holding = Holding::symbol('ACME')->portfolio($this->portfolio->id)->first(); $daily_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $first_transaction->date->copy()->nextWeekday()) ->first(); $this->assertEquals($holding->average_cost_basis, $daily_change->total_cost_basis); $second_transaction = Transaction::factory()->buy()->lastYear()->portfolio($this->portfolio->id)->symbol('ACME')->create(); $this->portfolio->syncDailyChanges(); $daily_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $second_transaction->date->copy()->nextWeekday()) ->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($this->portfolio->id)->symbol('ACME')->create()->first(); $this->portfolio->syncDailyChanges(); $daily_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $third_transaction->date->copy()->nextWeekday()) ->first(); $this->assertEqualsWithDelta(0, $daily_change->total_cost_basis, 0.01); } public function test_sales_are_captured_as_realized_gains(): void { Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('ACME')->create(); $sale_transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('ACME')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('GOOG')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('FOO')->create(); Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('BAR')->create(); $this->portfolio->syncDailyChanges(); $daily_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $sale_transaction->date->copy()->nextWeekday()) ->first(); $realized_gain = ($sale_transaction->sale_price - $sale_transaction->cost_basis) * $sale_transaction->quantity; $this->assertEqualsWithDelta($realized_gain, $daily_change->realized_gain_dollars, 0.01); $day_before = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $sale_transaction->date->copy()->previousWeekday()) ->first(); $this->assertEmpty($day_before->realized_gain_dollars); $after = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $sale_transaction->date->copy()->addDays(1)->nextWeekday()) ->first(); $this->assertEqualsWithDelta($realized_gain, $after->realized_gain_dollars, 0.01); } public function test_dividends_captured_in_daily_change_sync(): void { Transaction::factory(5)->buy()->yearsAgo()->portfolio($this->portfolio->id)->symbol('ACME')->create(); Artisan::call('refresh:dividend-data'); $this->portfolio->syncDailyChanges(); $holding = Holding::query()->portfolio($this->portfolio->id)->symbol('ACME')->first(); $dividends = $holding->dividends()->get()->sortBy('date'); $first_dividend_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $dividends->first()->date->nextWeekday()) ->first(); $owned = $dividends->first()->purchased - $dividends->first()->sold; $this->assertEqualsWithDelta($dividends->first()->dividend_amount * $owned, $first_dividend_change->total_dividends_earned, 0.01); $last_dividend_change = DailyChange::withDailyPerformance() ->portfolio($this->portfolio->id) ->whereDate('daily_change.date', '=', $dividends->last()->date->nextWeekday()) ->first(); $total_dividends = $dividends->reduce(function (?float $carry, $dividend) { return $carry + ($dividend['dividend_amount'] * ($dividend['purchased'] - $dividend['sold'])); }); $owned = $dividends->last()->purchased - $dividends->last()->sold; $this->assertEqualsWithDelta($total_dividends, $last_dividend_change->total_dividends_earned, 0.01); } public function test_daily_changes_synced_into_past_for_older_transactions(): void { $this->actingAs($user = User::factory()->create()); $portfolio = Portfolio::factory()->create(); // 1. test daily change will fill to the date of first transaction $first_transaction = Transaction::factory(5)->buy()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create(); $portfolio->syncDailyChanges(); $first_date = DailyChange::min('date'); $this->assertEquals($first_transaction->min('date')->toDateString(), $first_date); // 2. test daily change will fill when new transaction pre-dates earliest daily change config()->set('app.env', 'local'); $this->withoutDefer(); $second_transaction = Transaction::create([ 'symbol' => 'AAPL', 'portfolio_id' => $portfolio->id, 'date' => now()->subYears(3), 'quantity' => 1, 'cost_basis' => 39.89, 'transaction_type' => 'BUY', ]); $second_date = DailyChange::min('date'); $this->assertEquals($second_transaction->date->toDateString(), $second_date); // 3. test daily change will fill when new transaction is between earliest daily change and earliest transaction $third_transaction = Transaction::create([ 'symbol' => 'AAPL', 'portfolio_id' => $portfolio->id, 'date' => now()->subYears(1), 'quantity' => 1, 'cost_basis' => 39.89, 'transaction_type' => 'BUY', ]); $daily_change_day_after = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->nextWeekday())->first(); $daily_change_day_before = DailyChange::withDailyPerformance()->whereDate('daily_change.date', $third_transaction->date->previousWeekday())->first(); $this->assertNotNull($daily_change_day_after); $this->assertNotNull($daily_change_day_before); $this->assertEquals(39.89, $daily_change_day_after->total_cost_basis - $daily_change_day_before->total_cost_basis); } }