wip
This commit is contained in:
@@ -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']));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user