diff --git a/app/Exports/Sheets/DailyChangesSheet.php b/app/Exports/Sheets/DailyChangesSheet.php index a22786d..909e0c4 100644 --- a/app/Exports/Sheets/DailyChangesSheet.php +++ b/app/Exports/Sheets/DailyChangesSheet.php @@ -22,9 +22,8 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle 'Portfolio ID', 'Total Market Value', 'Total Cost Basis', - 'Total Gain', - 'Total Dividends Earned', 'Realized Gains', + 'Total Dividends Earned', 'Annotation', ]; } diff --git a/app/Models/DailyChange.php b/app/Models/DailyChange.php index 10a7fbb..bb6b96e 100644 --- a/app/Models/DailyChange.php +++ b/app/Models/DailyChange.php @@ -65,7 +65,7 @@ class DailyChange extends Model $dividendSub = DB::table('holdings') ->join('dividends', 'dividends.symbol', '=', 'holdings.symbol') ->leftJoin('currency_rates as cr', function ($join) use ($currency) { - $join->on('cr.date', '=', 'dividends.date') + $join->on(DB::raw('DATE(cr.date)'), '=', DB::raw('DATE(dividends.date)')) ->where('cr.currency', '=', $currency); }) ->join('transactions as tx', function ($join) { @@ -86,112 +86,78 @@ class DailyChange extends Model AS total_dividends_earned") ->groupBy(['holdings.portfolio_id', 'dividends.date', 'tx.transaction_type', 'tx.quantity']); - $totalCostBasisSub = DB::table('transactions as tx1') + $costBasisSub = DB::table('transactions') ->leftJoin('currency_rates as cr', function ($join) use ($currency) { - $join->on('cr.date', '=', 'tx1.date') - ->where('cr.currency', '=', $currency); + $join->on(DB::raw('DATE(cr.date)'), '=', DB::raw('DATE(transactions.date)')) + ->where('cr.currency', $currency); }) - ->select([ - 'tx1.portfolio_id', - 'tx1.date', - 'tx1.symbol', - 'tx1.transaction_type', - 'tx1.cost_basis_base', - 'tx1.quantity', - ]) - ->selectRaw("(CASE - WHEN - tx1.transaction_type = 'BUY' - OR SUM(tx1.cost_basis_base) = 0 - THEN - COALESCE(cr.rate, 1) - ELSE ( - SELECT - SUM(COALESCE(cr2.rate, 1) * buy.cost_basis_base) - / SUM(buy.cost_basis_base) - FROM transactions as buy - LEFT JOIN currency_rates as cr2 - ON cr2.date = buy.date - AND cr2.currency = '{$currency}' - WHERE buy.symbol = tx1.symbol - AND buy.portfolio_id = tx1.portfolio_id - AND buy.transaction_type = 'BUY' - AND buy.date <= tx1.date - ) END) - AS rate") - ->selectRaw( - "COALESCE(SUM(CASE - WHEN tx1.transaction_type = 'BUY' - THEN tx1.cost_basis_base * tx1.quantity - END), 0) - AS total_cost_basis_for_purchases" - ) - ->selectRaw( - "COALESCE(SUM(CASE - WHEN tx1.transaction_type = 'SELL' - THEN tx1.cost_basis_base * tx1.quantity - END), 0) - AS total_cost_basis_for_sales" - ) - ->selectRaw( - "(CASE - WHEN tx1.transaction_type = 'SELL' - THEN tx1.sale_price_base - tx1.cost_basis_base - ELSE 0 END) - * tx1.quantity - * COALESCE(cr.rate, 1) - AS realized_gain_dollars") - ->groupBy([ - 'tx1.portfolio_id', - 'tx1.date', - 'tx1.symbol', - 'tx1.transaction_type', - 'tx1.cost_basis_base', - 'tx1.quantity', - 'cr.rate', - 'tx1.sale_price_base', - ]); + ->select(['transactions.portfolio_id', 'transactions.date']); return $query ->select(['daily_change.date', 'daily_change.portfolio_id']) - ->leftJoinSub($totalCostBasisSub, 'cost_basis_display', function ($join) { - $join->on('daily_change.date', '>=', 'cost_basis_display.date') - ->whereColumn('daily_change.portfolio_id', '=', 'cost_basis_display.portfolio_id'); - }) + ->selectRaw('daily_change.total_market_value * COALESCE(cr.rate, 1) AS total_market_value') ->leftJoin('currency_rates as cr', function ($join) use ($currency) { - $join->on('cr.date', '=', 'daily_change.date') + $join->on(DB::raw('DATE(cr.date)'), '=', DB::raw('DATE(daily_change.date)')) ->where('cr.currency', '=', $currency); }) - ->selectRaw(' - SUM(cost_basis_display.rate * cost_basis_display.total_cost_basis_for_purchases) - SUM(cost_basis_display.rate * cost_basis_display.total_cost_basis_for_sales) - AS total_cost_basis - ') - ->selectRaw('( - daily_change.total_market_value * COALESCE(cr.rate, 1) - ) - (SUM(cost_basis_display.rate * cost_basis_display.total_cost_basis_for_purchases) - SUM(cost_basis_display.rate * cost_basis_display.total_cost_basis_for_sales)) - as total_gain') - ->selectRaw('( - daily_change.total_market_value * COALESCE(cr.rate, 1) - ) as total_market_value') - ->selectRaw(' - SUM( - cost_basis_display.realized_gain_dollars - ) as realized_gain_dollars') - ->selectSub(function ($query) use ($dividendSub) { + ->selectSub(function ($query) use ($costBasisSub) { + $query->fromSub( + $costBasisSub->selectRaw(" + (CASE + WHEN transactions.transaction_type = 'BUY' + THEN 1 ELSE -1 END + ) * transactions.cost_basis_base * transactions.quantity * COALESCE(cr.rate, 1) AS total_cost_basis"), + 'cb') + ->selectRaw('SUM(cb.total_cost_basis)') + ->whereColumn('cb.date', '<=', 'daily_change.date') + ->whereColumn('cb.portfolio_id', '=', 'daily_change.portfolio_id'); + }, 'total_cost_basis') + ->selectSub(function ($query) use ($costBasisSub) { + $query->fromSub( + $costBasisSub->selectRaw(" + (CASE + WHEN transactions.transaction_type = 'SELL' + THEN transactions.sale_price_base - transactions.cost_basis_base + END + ) * transactions.quantity * COALESCE(cr.rate, 1) AS realized_gain_loss"), + 'cb') + ->selectRaw('SUM(cb.realized_gain_loss)') + ->whereColumn('cb.date', '<=', 'daily_change.date') + ->whereColumn('cb.portfolio_id', '=', 'daily_change.portfolio_id'); + }, 'realized_gain_loss') + ->selectSub(function ($query) use ($dividendSub) { // todo: maybe costbasis uses this model? $query->fromSub($dividendSub, 'd') ->selectRaw('SUM(d.total_dividends_earned)') ->whereColumn('d.date', '<=', 'daily_change.date') ->whereColumn('d.portfolio_id', '=', 'daily_change.portfolio_id'); }, 'total_dividends_earned') - ->groupBy([ - 'daily_change.date', - 'cr.rate', - 'daily_change.total_market_value', - 'daily_change.portfolio_id', - ]) + ->addSelect('annotation') ->orderBy('daily_change.date'); } + public function scopeGetDailyPerformance($query) + { + return $query->get() + ->sortBy('date') + ->groupBy('date') + ->map(function ($group) { + + $total_market_value = $group->sum('total_market_value'); + $total_cost_basis = $group->sum('total_cost_basis'); + $total_market_gain = $total_market_value - $total_cost_basis; + + return (object) [ + 'date' => $group->first()->date->toDateString(), + 'total_market_value' => $total_market_value, + 'total_cost_basis' => $total_cost_basis, + 'total_gain' => $total_market_gain, + 'realized_gain_dollars' => $group->sum('realized_gain_dollars'), + 'total_dividends_earned' => $group->sum('total_dividends_earned'), + ]; + }) + ->values(); + } + public function portfolio() { return $this->belongsTo(Portfolio::class); diff --git a/resources/views/portfolio/portfolio-performance-chart.blade.php b/resources/views/portfolio/portfolio-performance-chart.blade.php index 0b3f32d..636cd51 100644 --- a/resources/views/portfolio/portfolio-performance-chart.blade.php +++ b/resources/views/portfolio/portfolio-performance-chart.blade.php @@ -53,22 +53,7 @@ new class extends Component $dailyChangeQuery->whereDate('daily_change.date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args'])); } - $dailyChange = $dailyChangeQuery->get(); - - $dailyChange = $dailyChange - ->sortBy('date') - ->groupBy('date') - ->map(function ($group) { - return (object) [ - 'date' => $group->first()->date->toDateString(), - 'total_market_value' => $group->sum('total_market_value'), - 'total_cost_basis' => $group->sum('total_cost_basis'), - 'total_gain' => $group->sum('total_gain'), - 'realized_gain_dollars' => $group->sum('realized_gain_dollars'), - 'total_dividends_earned' => $group->sum('total_dividends_earned'), - ]; - }) - ->values(); + $dailyChange = $dailyChangeQuery->getDailyPerformance(); return [ 'series' => [ diff --git a/tests/MultiCurrencyTest.php b/tests/MultiCurrencyTest.php index 17f7385..9e8335b 100644 --- a/tests/MultiCurrencyTest.php +++ b/tests/MultiCurrencyTest.php @@ -557,20 +557,9 @@ class MultiCurrencyTest extends TestCase $dailyChange = DailyChange::withDailyPerformance() ->portfolio($portfolio->id) - ->get(); + ->getDailyPerformance(); - $dailyChange = $dailyChange->sortBy('date') - ->groupBy('date') - ->map(function ($group) { - return (object) [ - 'date' => $group->first()->date->toDateString(), - 'total_market_value' => $group->sum('total_market_value'), - 'total_cost_basis' => $group->sum('total_cost_basis'), - 'total_gain' => $group->sum('total_gain'), - 'realized_gain_dollars' => $group->sum('realized_gain_dollars'), - 'total_dividends_earned' => $group->sum('total_dividends_earned'), - ]; - }); + dump($dailyChange->toArray()); $metrics = Holding::query() ->portfolio($portfolio->id) @@ -618,32 +607,16 @@ class MultiCurrencyTest extends TestCase $dailyChange = DailyChange::withDailyPerformance() ->portfolio($portfolio->id) - ->get(); - - dump($dailyChange->toArray()); - - $dailyChange = $dailyChange->sortBy('date') - ->groupBy('date') - ->map(function ($group) { - - return (object) [ - 'date' => $group->first()->date->toDateString(), - 'total_market_value' => $group->sum('total_market_value'), - 'total_cost_basis' => $group->sum('total_cost_basis'), - 'total_gain' => $group->sum('total_gain'), - 'realized_gain_dollars' => $group->sum('realized_gain_dollars'), - 'total_dividends_earned' => $group->sum('total_dividends_earned'), - ]; - }); + ->getDailyPerformance(); $metrics = Holding::query() ->portfolio($portfolio->id) ->getPortfolioMetrics(); - $this->assertEqualsWithDelta($metrics->get('total_market_value'), $dailyChange->last()->total_market_value, 0.01); // TODO: + $this->assertEqualsWithDelta($metrics->get('total_market_value'), $dailyChange->last()->total_market_value, 0.01); $this->assertEqualsWithDelta($metrics->get('total_cost_basis'), $dailyChange->last()->total_cost_basis, 0.01); $this->assertEqualsWithDelta($metrics->get('realized_gain_dollars'), $dailyChange->last()->realized_gain_dollars, 0.01); - $this->assertEqualsWithDelta($metrics->get('total_market_value') - $metrics->get('total_cost_basis'), $dailyChange->last()->total_gain, 0.01); // TODO: + $this->assertEqualsWithDelta($metrics->get('total_market_value') - $metrics->get('total_cost_basis'), $dailyChange->last()->total_gain, 0.01); } public function test_multi_currency_import_calculates_correct_holding_data(): void