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;
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']));
}
}
+2 -2
View File
@@ -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'],
]);
+7 -3
View File
@@ -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);
+8 -8
View File
@@ -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)
+5 -1
View File
@@ -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();
@@ -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();
@@ -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();
@@ -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');
@@ -16,7 +16,7 @@
<x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" />
</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>
@@ -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)
@@ -1,5 +1,6 @@
<?php
use App\Models\DailyChange;
use App\Models\Portfolio;
use Livewire\Attributes\{Title, Rule};
use Livewire\Volt\Component;
@@ -11,70 +12,83 @@ new class extends Component {
public String $name = 'portfolio';
public String $scope = 'YTD';
public Array $options = [
['id' => '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 {
</div>
<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">
@@ -110,7 +124,7 @@ new class extends Component {
<div
class="h-[280px] mb-5"
>
<x-ib-apex-chart :series-data="$myChart" :name="$name" />
<x-ib-apex-chart :series-data="$chartSeries" :name="$name" />
</div>
</x-card>
+6 -5
View File
@@ -53,29 +53,30 @@
<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">
<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 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="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 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="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 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="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 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="font-black text-xl"> {{ Number::currency($portfolio->dividendsEarned) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_dividends_earned) }} </div>
</x-card>
</div>