diff --git a/app/Console/Commands/CaptureDailyChange.php b/app/Console/Commands/CaptureDailyChange.php new file mode 100644 index 0000000..aaa87f0 --- /dev/null +++ b/app/Console/Commands/CaptureDailyChange.php @@ -0,0 +1,75 @@ +each(function($user){ + + $this->line('Capturing daily change for ' . $user->name); + + $portfolios = $user->portfolios()->withoutWishlists()->with(['holdings.market_data'])->get(); + + $total_cost_basis = $portfolios->reduce(function ($carry, $portfolio) { + return $carry + $portfolio->holdings->sum('total_cost_basis'); + }); + + $total_dividends = $portfolios->reduce(function ($carry, $portfolio) { + return $carry + $portfolio->holdings->sum('dividends_earned'); + }); + + $realized_gains = $portfolios->reduce(function ($carry, $portfolio) { + return $carry + $portfolio->holdings->sum('realized_gain_loss_dollars'); + }); + + $total_market_value = $portfolios->reduce(function ($carry, $portfolio) { + return $carry + $portfolio->holdings->sum(function($holding) { + return $holding->market_data->market_value * $holding->quantity; + }) ; + }); + + $user->daily_changes()->create([ + 'date' => now(), + 'total_cost_basis' => $total_cost_basis, + 'total_market_value' => $total_market_value, + 'total_dividends' => $total_dividends, + 'realized_gains' => $realized_gains, + 'total_gain_loss' => $total_market_value - $total_cost_basis + ]); + }); + } +} diff --git a/app/Console/Commands/RefreshDividendData.php b/app/Console/Commands/RefreshDividendData.php new file mode 100644 index 0000000..fa68707 --- /dev/null +++ b/app/Console/Commands/RefreshDividendData.php @@ -0,0 +1,50 @@ +', 0)->distinct()->get(['symbol']); + $holdings = Holding::distinct()->get(['symbol']); + + foreach ($holdings as $holding) { + $this->line('Refreshing ' . $holding->symbol); + Dividend::refreshDividendData($holding->symbol); + } + } +} diff --git a/app/Console/Commands/RefreshMarketData.php b/app/Console/Commands/RefreshMarketData.php new file mode 100644 index 0000000..f0c27c1 --- /dev/null +++ b/app/Console/Commands/RefreshMarketData.php @@ -0,0 +1,50 @@ +line('Refreshing ' . $symbol->symbol); + $symbol->refreshMarketData(); + } + } +} diff --git a/app/Console/Commands/RefreshSplitData.php b/app/Console/Commands/RefreshSplitData.php new file mode 100644 index 0000000..d7ded84 --- /dev/null +++ b/app/Console/Commands/RefreshSplitData.php @@ -0,0 +1,50 @@ +get(['symbol']); + + foreach ($holdings as $holding) { + $this->line('Refreshing ' . $holding->symbol); + Split::refreshSplitData($holding->symbol); + } + } +} diff --git a/app/Console/Commands/RefreshHoldingData.php b/app/Console/Commands/SyncHoldingData.php similarity index 87% rename from app/Console/Commands/RefreshHoldingData.php rename to app/Console/Commands/SyncHoldingData.php index a186b17..3174278 100644 --- a/app/Console/Commands/RefreshHoldingData.php +++ b/app/Console/Commands/SyncHoldingData.php @@ -3,11 +3,9 @@ namespace App\Console\Commands; use App\Models\Holding; -use App\Models\Dividend; -use App\Models\Transaction; use Illuminate\Console\Command; -class RefreshHoldingData extends Command +class SyncHoldingData extends Command { /** * The name and signature of the console command. @@ -46,7 +44,7 @@ class RefreshHoldingData extends Command foreach ($holdings as $holding) { $this->line('Refreshing ' . $holding->symbol); - $holding->sync(); + $holding->syncTransactionsAndDividends(); } } } diff --git a/app/Imports/BackupImport.php b/app/Imports/BackupImport.php index 7e5f07c..234ac02 100644 --- a/app/Imports/BackupImport.php +++ b/app/Imports/BackupImport.php @@ -2,19 +2,11 @@ namespace App\Imports; -use Illuminate\Support\Facades\DB; -use App\Imports\Sheets\SplitsSheet; -use App\Imports\Sheets\DividendsSheet; -use App\Imports\Sheets\MarketDataSheet; use App\Imports\Sheets\PortfoliosSheet; -use Illuminate\Support\Facades\Artisan; -use Maatwebsite\Excel\Events\AfterSheet; use App\Imports\Sheets\DailyChangesSheet; use App\Imports\Sheets\TransactionsSheet; -use Maatwebsite\Excel\Events\BeforeSheet; use Maatwebsite\Excel\Concerns\Importable; use Maatwebsite\Excel\Concerns\WithEvents; -use App\Console\Commands\RefreshHoldingData; use Maatwebsite\Excel\Concerns\WithMultipleSheets; class BackupImport implements WithMultipleSheets, WithEvents diff --git a/app/Imports/Sheets/TransactionsSheet.php b/app/Imports/Sheets/TransactionsSheet.php index 3613dfa..c8b1380 100644 --- a/app/Imports/Sheets/TransactionsSheet.php +++ b/app/Imports/Sheets/TransactionsSheet.php @@ -42,7 +42,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, return $transaction; }) - ->syncHolding(); + ->syncToHolding(); } }); diff --git a/app/Models/Dividend.php b/app/Models/Dividend.php index 31a4553..8879765 100644 --- a/app/Models/Dividend.php +++ b/app/Models/Dividend.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use App\Interfaces\MarketData\MarketDataInterface; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -26,38 +27,77 @@ class Dividend extends Model ]; /** - * Syncs all holdings of symbol with dividend data + * Grab new dividend data * - * @param array|self $model + * @param string $symbol * @return void */ - public static function syncHoldings(mixed $model) + public static function refreshDividendData(string $symbol) { - // check if we got an array, if yes then lets create a dummy model - if (is_array($model)) { - $model = (new self)->fill($model); + $dividends_meta = self::where(['symbol' => $symbol]) + ->selectRaw('COUNT(symbol) as total_dividends') + ->selectRaw('MAX(date) as last_date') + ->get() + ->first(); + + // assume we need to populate ALL dividend data + $start_date = new \DateTime('@0'); + $end_date = now(); + + // nope, refresh forward looking only + if ( $dividends_meta->total_dividends ) { + + $start_date = $dividends_meta->last_date->addHours(48); + $end_date = now(); } - // pull dividend data joined with holdings/transactions - $dividends = self::where(['dividends.symbol' => $model->symbol]) - ->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') - ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') - ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') - ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') - ->join('transactions', 'transactions.symbol', 'dividends.symbol') - ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') - ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->get(); + // get some data + if ($dividend_data = collect() && $start_date && $end_date) { + $dividend_data = app(MarketDataInterface::class)->dividends($symbol, $start_date, $end_date); + } - // iterate through holdings and update - Holding::where(['symbol' => $model->symbol]) - ->get() - ->each(function ($holding) use ($dividends) { - $holding->update([ - 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)->sum('dividends_received') - ]); - }); + // ah, we found some dividends... + if ($dividend_data->isNotEmpty()) { + // create mass insert + foreach ($dividend_data as $index => $dividend){ + $dividend_data[$index] = [...$dividend, ...['updated_at' => now(), 'created_at' => now()]]; + } + + // insert records + (new self)->insert($dividend_data->toArray()); + + // sync to holdings + $dividends = self::where([ + 'dividends.symbol' => $dividend_data->last()->symbol, + ])->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') + ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') + ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') + ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') + ->join('transactions', 'transactions.symbol', 'dividends.symbol') + ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') + ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->get(); + + // iterate through holdings and update + Holding::where(['symbol' => $symbol]) + ->get() + ->each(function ($holding) use ($dividends) { + $holding->update([ + 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)->sum('dividends_received') + ]); + }); + + // sync most last dividend date in market data + $market_data = MarketData::symbol($symbol)->first(); + $dividend_data_latest_date = $dividend_data->sortByDesc('date')->first()['date']; + + if ($market_data->dividend_date < $dividend_data_latest_date) { + $market_data->update(['dividend_date' => $dividend_data_latest_date]); // why is this set to latest date? + } + } + + return $dividend_data; } public function marketData() { diff --git a/app/Models/Holding.php b/app/Models/Holding.php index b87d6d9..9a7c0f5 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -165,7 +165,7 @@ class Holding extends Model ->join('market_data', 'market_data.symbol', 'holdings.symbol'); } - public function sync() + public function syncTransactionsAndDividends() { // pull existing transaction data $query = Transaction::where([ diff --git a/app/Models/MarketData.php b/app/Models/MarketData.php index 84a6070..3caac5c 100644 --- a/app/Models/MarketData.php +++ b/app/Models/MarketData.php @@ -34,19 +34,19 @@ class MarketData extends Model 'market_cap' => 0 ]; - public static function setSplitsHoldingSynced($symbol) + public function holdings() { - $market_data = self::where('symbol', $symbol)->get()->first(); + return $this->hasMany(Holding::class, 'symbol', 'symbol'); + } - $market_data->splits_synced_to_holdings_at = now(); - - $market_data->save(); + public function scopeSymbol($query, $symbol) + { + return $query->where('symbol', $symbol); } public function refreshMarketData() { return static::getMarketData($this->attributes['symbol']); - } public static function getMarketData($symbol) @@ -74,14 +74,4 @@ class MarketData extends Model return $market_data; } - - public function holdings() - { - return $this->hasMany(Holding::class, 'symbol', 'symbol'); - } - - public function scopeSymbol($query, $symbol) - { - return $query->where('symbol', $symbol); - } } \ No newline at end of file diff --git a/app/Models/Split.php b/app/Models/Split.php index 312180e..4e1d70c 100644 --- a/app/Models/Split.php +++ b/app/Models/Split.php @@ -28,6 +28,58 @@ class Split extends Model 'last_date' => 'datetime', ]; + public function holdings() { + return $this->hasMany(Holding::class, 'symbol', 'symbol'); + } + + public function transactions() { + return $this->hasMany(Transaction::class, 'symbol', 'symbol'); + } + + /** + * Grab new split data + * + * @param string $symbol + * @param \DateTimeInterface|null $start_date + * @return void + */ + public static function refreshSplitData(string $symbol) + { + // dates for split data + $splits_meta = self::where(['symbol' => $symbol]) + ->selectRaw('COUNT(symbol) as total_splits') + ->selectRaw('MIN(date) as first_date') + ->selectRaw('MAX(date) as last_date') + ->get() + ->first(); + + // assume need to populate all split data because it didnt exist before + $start_date = new \DateTime('@0'); + $end_date = now(); + + // nope, need to populate newer split data + if ($splits_meta->total_splits) { + + $start_date = $splits_meta->last_date->addHours(48); + $end_date = now(); + } + + // get some data + if ($split_data = collect() && $start_date && $end_date) { + $split_data = app(MarketDataInterface::class)->splits($symbol, $start_date, $end_date); + } + + if ($split_data->isNotEmpty()) { + // insert records + (new self)->insert($split_data->toArray()); + } + + // sync to transactions + self::syncToTransactions($symbol); + + return $split_data; + } + /** * Syncs all transactions of symbol with split data * @@ -77,60 +129,9 @@ class Split extends Model } } - // update market data with latest date - MarketData::setSplitsHoldingSynced($symbol); + // // update market data with latest date + // MarketData::setSplitsHoldingSynced($symbol); } - /** - * Grab new split data - * - * @param string $symbol - * @param \DateTimeInterface|null $start_date - * @return void - */ - public static function getSplitData(string $symbol) - { - // dates for split data - $splits_meta = self::where(['symbol' => $symbol]) - ->selectRaw('COUNT(symbol) as total_splits') - ->selectRaw('MIN(date) as first_date') - ->selectRaw('MAX(date) as last_date') - ->get() - ->first(); - - // assume need to populate all split data because it didnt exist before - $start_date = new \DateTime('@0'); - $end_date = now(); - - // nope, need to populate newer split data - if ($splits_meta->total_splits) { - - $start_date = $splits_meta->last_date->addHours(48); - $end_date = now(); - } - - // get some data - if ($split_data = collect() && $start_date && $end_date) { - $split_data = app(MarketDataInterface::class)->splits($symbol, $start_date, $end_date); - } - - if ($split_data->isNotEmpty()) { - // insert records - (new self)->insert($split_data->toArray()); - - } - - // sync to transactions - self::syncToTransactions($symbol); - - return $split_data; - } - - public function holdings() { - return $this->hasMany(Holding::class, 'symbol', 'symbol'); - } - - public function transactions() { - return $this->hasMany(Transaction::class, 'symbol', 'symbol'); - } + } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 455f1a7..52f352e 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -44,18 +44,16 @@ class Transaction extends Model static::saved(function ($transaction) { - $transaction->syncHolding(); + $transaction->syncToHolding(); $transaction->refreshMarketData(); - $transaction->syncDividendsToHolding(); - cache()->tags(['metrics', auth()->user()->id])->flush(); }); static::deleted(function ($transaction) { - $transaction->syncHolding(); + $transaction->syncToHolding(); cache()->tags(['metrics', auth()->user()->id])->flush(); }); @@ -124,11 +122,6 @@ class Transaction extends Model { return MarketData::getMarketData($this->attributes['symbol']); } - - public function syncDividendsToHolding() - { - return Dividend::syncHoldings(['symbol' => $this->attributes['symbol']]); - } /** * Writes average cost basis to a sale transaction @@ -154,7 +147,7 @@ class Transaction extends Model * * @return void */ - public function syncHolding() { + public function syncToHolding() { // if symbol name changed, sync previous symbol too if (Arr::has($this->changes, 'symbol')) { @@ -163,7 +156,7 @@ class Transaction extends Model $temp->symbol = $this->original['symbol']; $temp->portfolio_id = $this->portfolio_id; - $temp->syncHolding(); + $temp->syncToHolding(); } // get the holding for a symbol and portfolio (or create one) @@ -178,6 +171,6 @@ class Transaction extends Model 'total_cost_basis' => $this->quantity * $this->cost_basis, ]); - $holding->sync(); + $holding->syncTransactionsAndDividends(); } } \ No newline at end of file diff --git a/resources/views/components/gain-loss-arrow-badge.blade.php b/resources/views/components/gain-loss-arrow-badge.blade.php index 1fe4753..01b488e 100644 --- a/resources/views/components/gain-loss-arrow-badge.blade.php +++ b/resources/views/components/gain-loss-arrow-badge.blade.php @@ -7,7 +7,7 @@ } else { $isUp = $costBasis <= $marketValue; - $percent = ($marketValue - $costBasis) / $costBasis; + $percent = $costBasis ? (($marketValue - $costBasis) / $costBasis) : 0; } @endphp