diff --git a/app/Http/Controllers/PortfolioController.php b/app/Http/Controllers/PortfolioController.php index 682db86..aeeb9a3 100644 --- a/app/Http/Controllers/PortfolioController.php +++ b/app/Http/Controllers/PortfolioController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Models\Holding; use App\Models\Portfolio; +use App\Models\DailyChange; class PortfolioController extends Controller { @@ -21,12 +23,19 @@ class PortfolioController extends Controller public function show(Portfolio $portfolio) { - $portfolio->marketGainLoss = rand(-200, 3999); - $portfolio->totalCostBasis = rand(-200, 3999); - $portfolio->totalMarketValue = rand(-200, 3999); - $portfolio->realizedGainLoss = rand(-200, 3999); - $portfolio->dividendsEarned = rand(-200, 3999); - - return view('portfolio.show', compact(['portfolio'])); + // get portfolio metrics + $metrics = cache()->remember( + 'portfolio-metrics-' . $portfolio->id, + 60, + function () use ($portfolio) { + return + Holding::query() + ->portfolio($portfolio->id) + ->getPortfolioMetrics() + ->first(); + } + ); + + return view('portfolio.show', compact(['portfolio', 'metrics'])); } } diff --git a/app/Imports/Sheets/DailyChangesSheet.php b/app/Imports/Sheets/DailyChangesSheet.php index 4ad7b47..1760182 100644 --- a/app/Imports/Sheets/DailyChangesSheet.php +++ b/app/Imports/Sheets/DailyChangesSheet.php @@ -28,8 +28,8 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows 'date' => $row['date'], 'total_market_value' => $row['total_market_value'], 'total_cost_basis' => $row['total_cost_basis'], - 'total_gain_loss' => $row['total_gain_loss'], - 'total_dividends' => $row['total_dividends'], + 'total_gain' => $row['total_gain'], + 'total_dividends_earned' => $row['total_dividends_earned'], 'realized_gains' => $row['realized_gains'], 'notes' => $row['notes'], ]); diff --git a/app/Models/DailyChange.php b/app/Models/DailyChange.php index 0daaf64..e10ccf0 100644 --- a/app/Models/DailyChange.php +++ b/app/Models/DailyChange.php @@ -36,8 +36,8 @@ class DailyChange extends Model 'date', 'total_market_value', 'total_cost_basis', - 'total_gain_loss', - 'total_dividends', + 'total_gain', + 'total_dividends_earned', 'realized_gains', 'notes', ]; @@ -62,7 +62,11 @@ class DailyChange extends Model { return $query->where('user_id', auth()->user()->id); } - + + public function scopePortfolio($query, $portfolio) + { + return $query->where('portfolio_id', $portfolio); + } public function portfolio() { return $this->belongsTo(Portfolio::class); diff --git a/app/Models/Holding.php b/app/Models/Holding.php index 90fcbb9..9e4b378 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -25,7 +25,7 @@ class Holding extends Model 'quantity', 'average_cost_basis', 'total_cost_basis', - 'realized_gain_loss_dollars', + 'realized_gain_dollars', 'dividends_earned', 'splits_synced_at', 'dividends_synced_at' @@ -110,14 +110,14 @@ class Holding extends Model public function scopeGetPortfolioMetrics($query) { - $query->selectRaw('SUM(holdings.dividends_earned) AS total_dividends_earned') - ->selectRaw('SUM(holdings.realized_gain_loss_dollars) AS realized_gain_loss_dollars') - ->selectRaw('@total_market_value:=SUM(holdings.quantity * market_data.market_value) AS total_market_value') - ->selectRaw('@sum_total_cost_basis:=SUM(holdings.total_cost_basis) AS total_cost_basis') - ->selectRaw('@total_gain_loss_dollars:=(@total_market_value - @sum_total_cost_basis) AS total_gain_loss_dollars') - ->selectRaw('(@total_gain_loss_dollars / @sum_total_cost_basis) * 100 AS total_gain_loss_percent') + + $query->selectRaw('COALESCE(SUM(holdings.dividends_earned),0) AS total_dividends_earned') + ->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars),0) AS realized_gain_dollars') + ->selectRaw('@total_market_value:=COALESCE(SUM(holdings.quantity * market_data.market_value),0) AS total_market_value') + ->selectRaw('@sum_total_cost_basis:=COALESCE(SUM(holdings.total_cost_basis),0) AS total_cost_basis') + ->selectRaw('@total_gain_dollars:=COALESCE((@total_market_value - @sum_total_cost_basis),0) AS total_gain_dollars') + ->selectRaw('COALESCE((@total_gain_dollars / @sum_total_cost_basis) * 100,0) AS total_gain_percent') ->join('market_data', 'market_data.symbol', 'holdings.symbol'); - // =(VLOOKUP(if(today or end of year),'Daily Change'!$A:$B,2,false) - VLOOKUP(first of year),'Daily Change'!$A:$C,3,false)) / (SUMIFS(transactions.cost_basis_lot,transactions.date,"<"&date(left(D19,4)+1,1,1),transactions.type,"Buy")-SUMIFS(transactions.cost_basis_lot,transactions.date,"<"&date(left(D19,4)+1,1,1),transactions.type,"Sell"))-1 } public function scopeSymbol($query, $symbol) diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index fcd8b52..4265625 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -44,11 +44,15 @@ class Transaction extends Model static::saved(function ($transaction) { $transaction->syncHolding(); + + cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); }); static::deleted(function ($transaction) { $transaction->syncHolding(); + + cache()->forget('portfolio-metrics-' . $transaction->portfolio_id); }); } @@ -171,7 +175,7 @@ class Transaction extends Model 'quantity' => $total_quantity, 'average_cost_basis' => $average_cost_basis, 'total_cost_basis' => $total_quantity * $average_cost_basis, - 'realized_gain_loss_dollars' => $query->realized_gains, + 'realized_gain_dollars' => $query->realized_gains, ]); $holding->save(); diff --git a/database/migrations/2021_02_25_041227_create_daily_change_table.php b/database/migrations/2021_02_25_041227_create_daily_change_table.php index 65b9d86..5e0e413 100644 --- a/database/migrations/2021_02_25_041227_create_daily_change_table.php +++ b/database/migrations/2021_02_25_041227_create_daily_change_table.php @@ -19,8 +19,8 @@ class CreateDailyChangeTable extends Migration $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->float('total_market_value', 12, 4)->nullable(); $table->float('total_cost_basis', 12, 4)->nullable(); - $table->float('total_gain_loss', 12, 4)->nullable(); - $table->float('total_dividends', 12, 4)->nullable(); + $table->float('total_gain', 12, 4)->nullable(); + $table->float('total_dividends_earned', 12, 4)->nullable(); $table->float('realized_gains', 12, 4)->nullable(); $table->text('annotation')->nullable(); diff --git a/database/migrations/2021_09_06_014744_create_holdings_table.php b/database/migrations/2021_09_06_014744_create_holdings_table.php index 17f24e0..3031330 100644 --- a/database/migrations/2021_09_06_014744_create_holdings_table.php +++ b/database/migrations/2021_09_06_014744_create_holdings_table.php @@ -22,7 +22,7 @@ class CreateHoldingsTable extends Migration $table->float('quantity', 12, 4); $table->float('average_cost_basis', 12, 4); $table->float('total_cost_basis', 12, 4)->nullable(); - $table->float('realized_gain_loss_dollars', 12, 4)->nullable(); + $table->float('realized_gain_dollars', 12, 4)->nullable(); $table->float('dividends_earned', 12, 4)->nullable(); $table->timestamp('splits_synced_at')->nullable(); $table->timestamp('dividends_synced_at')->nullable(); diff --git a/resources/views/components/ib-apex-chart.blade.php b/resources/views/components/ib-apex-chart.blade.php index c516b90..0139053 100644 --- a/resources/views/components/ib-apex-chart.blade.php +++ b/resources/views/components/ib-apex-chart.blade.php @@ -4,7 +4,7 @@ $seriesData = array_merge([ 'chart' => [ 'type' => "area", - 'stacked' => true, + 'stacked' => false, 'height' => 300, 'foreColor' => "#999", 'dropShadow' => [ @@ -13,8 +13,11 @@ 'toolbar' => [ 'show' => false, ], + 'zoom' => [ + 'enabled' => false + ] ], - 'colors' => ['#00E396', '#0090FF'], + 'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F', ], 'stroke' => [ 'curve' => "smooth", 'width' => 3 @@ -122,7 +125,7 @@ legendContainer.innerHTML = ''; // Clear any existing legend items chartContext.w.globals.seriesNames.forEach(function (seriesName, i) { - + var seriesColor = chartContext.w.config.colors[i]; var legendItem = document.createElement('div'); legendItem.classList.add('flex', 'items-center', 'm-2', 'cursor-pointer'); diff --git a/resources/views/components/partials/side-bar.blade.php b/resources/views/components/partials/side-bar.blade.php index 63cab39..4b8e206 100644 --- a/resources/views/components/partials/side-bar.blade.php +++ b/resources/views/components/partials/side-bar.blade.php @@ -16,7 +16,7 @@ - + {{-- --}} {{-- --}} diff --git a/resources/views/livewire/holdings-table.blade.php b/resources/views/livewire/holdings-table.blade.php index dd5b46b..9b84e49 100644 --- a/resources/views/livewire/holdings-table.blade.php +++ b/resources/views/livewire/holdings-table.blade.php @@ -25,9 +25,9 @@ new class extends Component { ['key' => 'total_cost_basis', 'label' => __('Total Cost Basis')], ['key' => 'market_data_market_value', 'label' => __('Market Value')], ['key' => 'total_market_value', 'label' => __('Total Market Value')], - ['key' => 'market_gain_loss_dollars', 'label' => __('Market Gain/Loss')], - ['key' => 'market_gain_loss_percent', 'label' => __('Market Gain/Loss')], - ['key' => 'realized_gain_loss_dollars', 'label' => __('Realized Gain/Loss')], + ['key' => 'market_gain_dollars', 'label' => __('Market Gain/Loss')], + ['key' => 'market_gain_percent', 'label' => __('Market Gain/Loss')], + ['key' => 'realized_gain_dollars', 'label' => __('Realized Gain/Loss')], ['key' => 'dividends_earned', 'label' => __('Dividends Earned')], ['key' => 'market_data_fifty_two_week_low', 'label' => __('52 week low')], ['key' => 'market_data_fifty_two_week_high', 'label' => __('52 week high')], @@ -40,9 +40,6 @@ new class extends Component { { return $this->portfolio ->holdings() - ->with(['transactions' => function ($query) { - $query->portfolio($this->portfolio->id); - }]) ->withCount(['transactions as num_transactions' => function ($query) { $query->portfolio($this->portfolio->id); }]) @@ -52,8 +49,8 @@ new class extends Component { ->withAggregate('market_data', 'fifty_two_week_high') ->withAggregate('market_data', 'updated_at') ->selectRaw('(market_data.market_value * holdings.quantity) AS total_market_value') - ->selectRaw('((market_data.market_value - holdings.average_cost_basis) * holdings.quantity) AS market_gain_loss_dollars') - ->selectRaw('(((market_data.market_value - holdings.average_cost_basis) / holdings.average_cost_basis) * 100) AS market_gain_loss_percent') + ->selectRaw('((market_data.market_value - holdings.average_cost_basis) * holdings.quantity) AS market_gain_dollars') + ->selectRaw('(((market_data.market_value - holdings.average_cost_basis) / holdings.average_cost_basis) * 100) AS market_gain_percent') ->join('market_data', 'holdings.symbol', 'market_data.symbol') ->orderBy(...array_values($this->sortBy)) ->where('quantity', '>', 0) diff --git a/resources/views/livewire/portfolio-performance-chart.blade.php b/resources/views/livewire/portfolio-performance-chart.blade.php index 8cff69a..272f284 100644 --- a/resources/views/livewire/portfolio-performance-chart.blade.php +++ b/resources/views/livewire/portfolio-performance-chart.blade.php @@ -1,5 +1,6 @@ '1M', 'name' => '1 month'], - ['id' => '3M', 'name' => '3 months'], - ['id' => 'YTD', 'name' => 'Year to date'], - ['id' => '1Y', 'name' => '1 year'], - ['id' => '3Y', 'name' => '3 years'], - ['id' => 'ALL', 'name' => 'All time'] + ['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' => []], + ['id' => '1Y', 'name' => '1 year', 'method' => 'subYears', 'args' => [1]], + ['id' => '3Y', 'name' => '3 years', 'method' => 'subYears', 'args' => [3]], + ['id' => 'ALL', 'name' => 'All time', 'method' => null] ]; // data - public Array $myChart; + public Array $chartSeries; // methods public function mount() { - $this->myChart = [ + $this->chartSeries = $this->generatePerformanceData(); + } + + public function generatePerformanceData() + { + $filterMethod = collect($this->options)->where('id', $this->scope)->first(); + + $dailyChangeQuery = DailyChange::query(); + + if (isset($this->portfolio)) { + $dailyChangeQuery->portfolio($this->portfolio->id); + } else { + $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, + SUM(total_dividends_earned) as total_dividends_earned, + SUM(realized_gains) as realized_gains' + )->groupBy('date'); + } + + if ($filterMethod['method']) { + $dailyChangeQuery->whereDate('date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args'])); + } + + $dailyChange = $dailyChangeQuery->get(); + + return [ 'series' => [ [ - 'name' => __('Total Views'), - 'data' => $this->generateDateSeries('2024-01-01', '2024-08-01') - ], + 'name' => __('Market Value'), + 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_market_value])->toArray(), + ], [ - 'name' => __('Second Views'), - 'data' => $this->generateDateSeries('2024-01-01', '2024-08-01') - ], - ], - + 'name' => __('Cost Basis'), + 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_cost_basis])->toArray(), + ], + [ + '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' => __('Realized Gains'), + 'data' => $dailyChange->map(fn($data) => [$data->date, $data->realized_gains])->toArray() + ], + ] ]; - - // $this->marketGainLoss = rand(-200, 3999); } public function changeScope($scope) { $this->scope = $scope; - $this->dispatch('data-scope-updated', $scope); + $this->chartSeries = $this->generatePerformanceData(); } public function getScopeName($scope) { - return collect($this->options)->where('id', $scope)['name']; - } - - private function generateDateSeries($startDate, $endDate) - { - $dateArray = []; - $currentDate = strtotime($startDate); - $endDate = strtotime($endDate); - - while ($currentDate <= $endDate) { - // Generate a random integer - $randomInt = rand(1000, 3000); - - // Format the current date to 'Y-m-d' - $formattedDate = date('Y-m-d', $currentDate); - - // Append the date and random integer to the array - $dateArray[] = [$formattedDate, $randomInt]; - - // Move to the next day - $currentDate = strtotime("+1 day", $currentDate); - } - - return $dateArray; + return collect($this->options)->where('id', $scope)->first()['name']; } }; ?> @@ -90,7 +104,7 @@ new class extends Component {
- + {{-- --}} @@ -110,7 +124,7 @@ new class extends Component {
- +
\ No newline at end of file diff --git a/resources/views/portfolio/show.blade.php b/resources/views/portfolio/show.blade.php index 8a7008b..b42a86f 100644 --- a/resources/views/portfolio/show.blade.php +++ b/resources/views/portfolio/show.blade.php @@ -53,29 +53,30 @@
+
{{ __('Market Gain/Loss') }}
-
{{ Number::currency($portfolio->marketGainLoss) }}
+
{{ Number::currency($metrics->total_gain_dollars) }}
{{ __('Total Cost Basis') }}
-
{{ Number::currency($portfolio->totalCostBasis) }}
+
{{ Number::currency($metrics->total_cost_basis) }}
{{ __('Total Market Value') }}
-
{{ Number::currency($portfolio->totalMarketValue) }}
+
{{ Number::currency($metrics->total_market_value) }}
{{ __('Realized Gain/Loss') }}
-
{{ Number::currency($portfolio->realizedGainLoss) }}
+
{{ Number::currency($metrics->realized_gain_dollars) }}
{{ __('Dividends Earned') }}
-
{{ Number::currency($portfolio->dividendsEarned) }}
+
{{ Number::currency($metrics->total_dividends_earned) }}