This commit is contained in:
hackerESQ
2024-08-21 20:42:32 -05:00
parent b84bc94da6
commit 58533a454d
12 changed files with 118 additions and 86 deletions
+16 -7
View File
@@ -2,7 +2,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Holding;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\DailyChange;
class PortfolioController extends Controller class PortfolioController extends Controller
{ {
@@ -21,12 +23,19 @@ class PortfolioController extends Controller
public function show(Portfolio $portfolio) public function show(Portfolio $portfolio)
{ {
$portfolio->marketGainLoss = rand(-200, 3999); // get portfolio metrics
$portfolio->totalCostBasis = rand(-200, 3999); $metrics = cache()->remember(
$portfolio->totalMarketValue = rand(-200, 3999); 'portfolio-metrics-' . $portfolio->id,
$portfolio->realizedGainLoss = rand(-200, 3999); 60,
$portfolio->dividendsEarned = rand(-200, 3999); function () use ($portfolio) {
return
return view('portfolio.show', compact(['portfolio'])); Holding::query()
->portfolio($portfolio->id)
->getPortfolioMetrics()
->first();
}
);
return view('portfolio.show', compact(['portfolio', 'metrics']));
} }
} }
+2 -2
View File
@@ -28,8 +28,8 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, SkipsEmptyRows
'date' => $row['date'], 'date' => $row['date'],
'total_market_value' => $row['total_market_value'], 'total_market_value' => $row['total_market_value'],
'total_cost_basis' => $row['total_cost_basis'], 'total_cost_basis' => $row['total_cost_basis'],
'total_gain_loss' => $row['total_gain_loss'], 'total_gain' => $row['total_gain'],
'total_dividends' => $row['total_dividends'], 'total_dividends_earned' => $row['total_dividends_earned'],
'realized_gains' => $row['realized_gains'], 'realized_gains' => $row['realized_gains'],
'notes' => $row['notes'], 'notes' => $row['notes'],
]); ]);
+7 -3
View File
@@ -36,8 +36,8 @@ class DailyChange extends Model
'date', 'date',
'total_market_value', 'total_market_value',
'total_cost_basis', 'total_cost_basis',
'total_gain_loss', 'total_gain',
'total_dividends', 'total_dividends_earned',
'realized_gains', 'realized_gains',
'notes', 'notes',
]; ];
@@ -62,7 +62,11 @@ class DailyChange extends Model
{ {
return $query->where('user_id', auth()->user()->id); return $query->where('user_id', auth()->user()->id);
} }
public function scopePortfolio($query, $portfolio)
{
return $query->where('portfolio_id', $portfolio);
}
public function portfolio() public function portfolio()
{ {
return $this->belongsTo(Portfolio::class); return $this->belongsTo(Portfolio::class);
+8 -8
View File
@@ -25,7 +25,7 @@ class Holding extends Model
'quantity', 'quantity',
'average_cost_basis', 'average_cost_basis',
'total_cost_basis', 'total_cost_basis',
'realized_gain_loss_dollars', 'realized_gain_dollars',
'dividends_earned', 'dividends_earned',
'splits_synced_at', 'splits_synced_at',
'dividends_synced_at' 'dividends_synced_at'
@@ -110,14 +110,14 @@ class Holding extends Model
public function scopeGetPortfolioMetrics($query) 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') $query->selectRaw('COALESCE(SUM(holdings.dividends_earned),0) AS total_dividends_earned')
->selectRaw('@total_market_value:=SUM(holdings.quantity * market_data.market_value) AS total_market_value') ->selectRaw('COALESCE(SUM(holdings.realized_gain_dollars),0) AS realized_gain_dollars')
->selectRaw('@sum_total_cost_basis:=SUM(holdings.total_cost_basis) AS total_cost_basis') ->selectRaw('@total_market_value:=COALESCE(SUM(holdings.quantity * market_data.market_value),0) AS total_market_value')
->selectRaw('@total_gain_loss_dollars:=(@total_market_value - @sum_total_cost_basis) AS total_gain_loss_dollars') ->selectRaw('@sum_total_cost_basis:=COALESCE(SUM(holdings.total_cost_basis),0) AS total_cost_basis')
->selectRaw('(@total_gain_loss_dollars / @sum_total_cost_basis) * 100 AS total_gain_loss_percent') ->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'); ->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) public function scopeSymbol($query, $symbol)
+5 -1
View File
@@ -44,11 +44,15 @@ class Transaction extends Model
static::saved(function ($transaction) { static::saved(function ($transaction) {
$transaction->syncHolding(); $transaction->syncHolding();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id);
}); });
static::deleted(function ($transaction) { static::deleted(function ($transaction) {
$transaction->syncHolding(); $transaction->syncHolding();
cache()->forget('portfolio-metrics-' . $transaction->portfolio_id);
}); });
} }
@@ -171,7 +175,7 @@ class Transaction extends Model
'quantity' => $total_quantity, 'quantity' => $total_quantity,
'average_cost_basis' => $average_cost_basis, 'average_cost_basis' => $average_cost_basis,
'total_cost_basis' => $total_quantity * $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(); $holding->save();
@@ -19,8 +19,8 @@ class CreateDailyChangeTable extends Migration
$table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade'); $table->foreignIdFor(Portfolio::class, 'portfolio_id')->constrained()->onDelete('cascade');
$table->float('total_market_value', 12, 4)->nullable(); $table->float('total_market_value', 12, 4)->nullable();
$table->float('total_cost_basis', 12, 4)->nullable(); $table->float('total_cost_basis', 12, 4)->nullable();
$table->float('total_gain_loss', 12, 4)->nullable(); $table->float('total_gain', 12, 4)->nullable();
$table->float('total_dividends', 12, 4)->nullable(); $table->float('total_dividends_earned', 12, 4)->nullable();
$table->float('realized_gains', 12, 4)->nullable(); $table->float('realized_gains', 12, 4)->nullable();
$table->text('annotation')->nullable(); $table->text('annotation')->nullable();
@@ -22,7 +22,7 @@ class CreateHoldingsTable extends Migration
$table->float('quantity', 12, 4); $table->float('quantity', 12, 4);
$table->float('average_cost_basis', 12, 4); $table->float('average_cost_basis', 12, 4);
$table->float('total_cost_basis', 12, 4)->nullable(); $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->float('dividends_earned', 12, 4)->nullable();
$table->timestamp('splits_synced_at')->nullable(); $table->timestamp('splits_synced_at')->nullable();
$table->timestamp('dividends_synced_at')->nullable(); $table->timestamp('dividends_synced_at')->nullable();
@@ -4,7 +4,7 @@
$seriesData = array_merge([ $seriesData = array_merge([
'chart' => [ 'chart' => [
'type' => "area", 'type' => "area",
'stacked' => true, 'stacked' => false,
'height' => 300, 'height' => 300,
'foreColor' => "#999", 'foreColor' => "#999",
'dropShadow' => [ 'dropShadow' => [
@@ -13,8 +13,11 @@
'toolbar' => [ 'toolbar' => [
'show' => false, 'show' => false,
], ],
'zoom' => [
'enabled' => false
]
], ],
'colors' => ['#00E396', '#0090FF'], 'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F', ],
'stroke' => [ 'stroke' => [
'curve' => "smooth", 'curve' => "smooth",
'width' => 3 'width' => 3
@@ -122,7 +125,7 @@
legendContainer.innerHTML = ''; // Clear any existing legend items legendContainer.innerHTML = ''; // Clear any existing legend items
chartContext.w.globals.seriesNames.forEach(function (seriesName, i) { chartContext.w.globals.seriesNames.forEach(function (seriesName, i) {
var seriesColor = chartContext.w.config.colors[i]; var seriesColor = chartContext.w.config.colors[i];
var legendItem = document.createElement('div'); var legendItem = document.createElement('div');
legendItem.classList.add('flex', 'items-center', 'm-2', 'cursor-pointer'); legendItem.classList.add('flex', 'items-center', 'm-2', 'cursor-pointer');
@@ -16,7 +16,7 @@
<x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" /> <x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" />
</x-menu-sub> </x-menu-sub>
<x-menu-item title="{{ __('Transactions') }}" icon="o-banknotes" link="{{ route('transaction.index') }}" /> {{-- <x-menu-item title="{{ __('Transactions') }}" icon="o-banknotes" link="{{ route('transaction.index') }}" /> --}}
{{-- <x-menu-item title="{{ __('Reporting') }}" icon="o-chart-bar-square" link="####" /> --}} {{-- <x-menu-item title="{{ __('Reporting') }}" icon="o-chart-bar-square" link="####" /> --}}
</x-menu> </x-menu>
@@ -25,9 +25,9 @@ new class extends Component {
['key' => 'total_cost_basis', 'label' => __('Total Cost Basis')], ['key' => 'total_cost_basis', 'label' => __('Total Cost Basis')],
['key' => 'market_data_market_value', 'label' => __('Market Value')], ['key' => 'market_data_market_value', 'label' => __('Market Value')],
['key' => 'total_market_value', 'label' => __('Total Market Value')], ['key' => 'total_market_value', 'label' => __('Total Market Value')],
['key' => 'market_gain_loss_dollars', 'label' => __('Market Gain/Loss')], ['key' => 'market_gain_dollars', 'label' => __('Market Gain/Loss')],
['key' => 'market_gain_loss_percent', 'label' => __('Market Gain/Loss')], ['key' => 'market_gain_percent', 'label' => __('Market Gain/Loss')],
['key' => 'realized_gain_loss_dollars', 'label' => __('Realized Gain/Loss')], ['key' => 'realized_gain_dollars', 'label' => __('Realized Gain/Loss')],
['key' => 'dividends_earned', 'label' => __('Dividends Earned')], ['key' => 'dividends_earned', 'label' => __('Dividends Earned')],
['key' => 'market_data_fifty_two_week_low', 'label' => __('52 week low')], ['key' => 'market_data_fifty_two_week_low', 'label' => __('52 week low')],
['key' => 'market_data_fifty_two_week_high', 'label' => __('52 week high')], ['key' => 'market_data_fifty_two_week_high', 'label' => __('52 week high')],
@@ -40,9 +40,6 @@ new class extends Component {
{ {
return $this->portfolio return $this->portfolio
->holdings() ->holdings()
->with(['transactions' => function ($query) {
$query->portfolio($this->portfolio->id);
}])
->withCount(['transactions as num_transactions' => function ($query) { ->withCount(['transactions as num_transactions' => function ($query) {
$query->portfolio($this->portfolio->id); $query->portfolio($this->portfolio->id);
}]) }])
@@ -52,8 +49,8 @@ new class extends Component {
->withAggregate('market_data', 'fifty_two_week_high') ->withAggregate('market_data', 'fifty_two_week_high')
->withAggregate('market_data', 'updated_at') ->withAggregate('market_data', 'updated_at')
->selectRaw('(market_data.market_value * holdings.quantity) AS total_market_value') ->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.quantity) AS market_gain_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.average_cost_basis) * 100) AS market_gain_percent')
->join('market_data', 'holdings.symbol', 'market_data.symbol') ->join('market_data', 'holdings.symbol', 'market_data.symbol')
->orderBy(...array_values($this->sortBy)) ->orderBy(...array_values($this->sortBy))
->where('quantity', '>', 0) ->where('quantity', '>', 0)
@@ -1,5 +1,6 @@
<?php <?php
use App\Models\DailyChange;
use App\Models\Portfolio; use App\Models\Portfolio;
use Livewire\Attributes\{Title, Rule}; use Livewire\Attributes\{Title, Rule};
use Livewire\Volt\Component; use Livewire\Volt\Component;
@@ -11,70 +12,83 @@ new class extends Component {
public String $name = 'portfolio'; public String $name = 'portfolio';
public String $scope = 'YTD'; public String $scope = 'YTD';
public Array $options = [ public Array $options = [
['id' => '1M', 'name' => '1 month'], ['id' => '1M', 'name' => '1 month', 'method' => 'subMonths', 'args' => [1]],
['id' => '3M', 'name' => '3 months'], ['id' => '3M', 'name' => '3 months', 'method' => 'subMonths', 'args' => [3]],
['id' => 'YTD', 'name' => 'Year to date'], ['id' => 'YTD', 'name' => 'Year to date', 'method' => 'startOfYear', 'args' => []],
['id' => '1Y', 'name' => '1 year'], ['id' => '1Y', 'name' => '1 year', 'method' => 'subYears', 'args' => [1]],
['id' => '3Y', 'name' => '3 years'], ['id' => '3Y', 'name' => '3 years', 'method' => 'subYears', 'args' => [3]],
['id' => 'ALL', 'name' => 'All time'] ['id' => 'ALL', 'name' => 'All time', 'method' => null]
]; ];
// data // data
public Array $myChart; public Array $chartSeries;
// methods // methods
public function mount() 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' => [ 'series' => [
[ [
'name' => __('Total Views'), 'name' => __('Market Value'),
'data' => $this->generateDateSeries('2024-01-01', '2024-08-01') 'data' => $dailyChange->map(fn($data) => [$data->date, $data->total_market_value])->toArray(),
], ],
[ [
'name' => __('Second Views'), 'name' => __('Cost Basis'),
'data' => $this->generateDateSeries('2024-01-01', '2024-08-01') '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) public function changeScope($scope)
{ {
$this->scope = $scope; $this->scope = $scope;
$this->dispatch('data-scope-updated', $scope); $this->chartSeries = $this->generatePerformanceData();
} }
public function getScopeName($scope) public function getScopeName($scope)
{ {
return collect($this->options)->where('id', $scope)['name']; return collect($this->options)->where('id', $scope)->first()['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;
} }
}; ?> }; ?>
@@ -90,7 +104,7 @@ new class extends Component {
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<x-button title="{{ __('Reset chart') }}" icon="o-arrow-path" class="btn-ghost btn-sm btn-circle mr-2" id="chart-reset-zoom-{{ $name }}" /> {{-- <x-button title="{{ __('Reset chart') }}" icon="o-arrow-path" class="btn-ghost btn-sm btn-circle mr-2" id="chart-reset-zoom-{{ $name }}" /> --}}
<x-dropdown title="{{ __('Choose time period') }}" label="{{ $scope }}" class="btn-ghost btn-sm"> <x-dropdown title="{{ __('Choose time period') }}" label="{{ $scope }}" class="btn-ghost btn-sm">
@@ -110,7 +124,7 @@ new class extends Component {
<div <div
class="h-[280px] mb-5" class="h-[280px] mb-5"
> >
<x-ib-apex-chart :series-data="$myChart" :name="$name" /> <x-ib-apex-chart :series-data="$chartSeries" :name="$name" />
</div> </div>
</x-card> </x-card>
+6 -5
View File
@@ -53,29 +53,30 @@
<div class="grid sm:grid-cols-5 gap-5"> <div class="grid sm:grid-cols-5 gap-5">
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg"> <x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Market Gain/Loss') }}</div> <div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Market Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($portfolio->marketGainLoss) }} </div> <div class="font-black text-xl"> {{ Number::currency($metrics->total_gain_dollars) }} </div>
</x-card> </x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg"> <x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Total Cost Basis') }}</div> <div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Total Cost Basis') }}</div>
<div class="font-black text-xl"> {{ Number::currency($portfolio->totalCostBasis) }} </div> <div class="font-black text-xl"> {{ Number::currency($metrics->total_cost_basis) }} </div>
</x-card> </x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg"> <x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Total Market Value') }}</div> <div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Total Market Value') }}</div>
<div class="font-black text-xl"> {{ Number::currency($portfolio->totalMarketValue) }} </div> <div class="font-black text-xl"> {{ Number::currency($metrics->total_market_value) }} </div>
</x-card> </x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg"> <x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Realized Gain/Loss') }}</div> <div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Realized Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($portfolio->realizedGainLoss) }} </div> <div class="font-black text-xl"> {{ Number::currency($metrics->realized_gain_dollars) }} </div>
</x-card> </x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg"> <x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Dividends Earned') }}</div> <div class="text-sm text-gray-400 whitespace-nowrap">{{ __('Dividends Earned') }}</div>
<div class="font-black text-xl"> {{ Number::currency($portfolio->dividendsEarned) }} </div> <div class="font-black text-xl"> {{ Number::currency($metrics->total_dividends_earned) }} </div>
</x-card> </x-card>
</div> </div>