simplify tests and daily change calculations
This commit is contained in:
+15
-23
@@ -71,7 +71,7 @@ class Holding extends Model
|
||||
CASE WHEN transaction_type = 'BUY'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND dividends.date >= transactions.date
|
||||
AND date(dividends.date) >= date(transactions.date)
|
||||
THEN transactions.quantity
|
||||
ELSE 0 END
|
||||
) AS purchased")
|
||||
@@ -79,10 +79,19 @@ class Holding extends Model
|
||||
CASE WHEN transaction_type = 'SELL'
|
||||
AND transactions.symbol = dividends.symbol
|
||||
AND transactions.portfolio_id = '$this->portfolio_id'
|
||||
AND dividends.date >= transactions.date
|
||||
AND date(dividends.date) >= date(transactions.date)
|
||||
THEN transactions.quantity
|
||||
ELSE 0 END
|
||||
) AS sold")
|
||||
->selectRaw('SUM(
|
||||
(CASE WHEN transaction_type = "BUY"
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END
|
||||
- CASE WHEN transaction_type = "SELL"
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END)
|
||||
* dividends.dividend_amount
|
||||
) AS total_received')
|
||||
->join('transactions', 'transactions.symbol', 'dividends.symbol')
|
||||
->groupBy(['dividends.symbol','dividends.date','dividends.dividend_amount'])
|
||||
->orderBy('dividends.date', 'DESC')
|
||||
@@ -186,32 +195,15 @@ class Holding extends Model
|
||||
? $query->total_cost_basis / $query->qty_purchases
|
||||
: 0;
|
||||
|
||||
// pull dividend data joined with holdings/transactions
|
||||
$dividends = Dividend::select('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount')
|
||||
->selectRaw('
|
||||
(COALESCE(CASE WHEN transactions.transaction_type = "BUY"
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END, 0)
|
||||
- COALESCE(CASE WHEN transactions.transaction_type = "SELL"
|
||||
AND date(transactions.date) <= date(dividends.date)
|
||||
THEN transactions.quantity ELSE 0 END, 0))
|
||||
* dividends.dividend_amount
|
||||
AS total_received
|
||||
')
|
||||
->join('transactions', 'transactions.symbol', 'dividends.symbol')
|
||||
->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id')
|
||||
->where('dividends.symbol', $this->symbol)
|
||||
->where('transactions.portfolio_id', $this->portfolio_id)
|
||||
->groupBy('holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount', 'total_received')
|
||||
->get();
|
||||
|
||||
// update holding
|
||||
$this->fill([
|
||||
'quantity' => $total_quantity,
|
||||
'average_cost_basis' => $average_cost_basis,
|
||||
'total_cost_basis' => $total_quantity * $average_cost_basis,
|
||||
'realized_gain_dollars' => $query->total_sale_price > 0 ? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases)) : 0,
|
||||
'dividends_earned' => $dividends->sum('total_received')
|
||||
'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0
|
||||
? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases))
|
||||
: 0,
|
||||
'dividends_earned' => $this->dividends->sum('total_received')
|
||||
]);
|
||||
|
||||
$this->save();
|
||||
|
||||
@@ -127,15 +127,17 @@ class Portfolio extends Model
|
||||
$total_market_value = $performance->owned * $close;
|
||||
$dividends_earned += $performance->owned * ($dividends->get($date)?->dividend_amount ?? 0);
|
||||
|
||||
$daily[$date] = [
|
||||
'date' => $date,
|
||||
'portfolio_id' => $this->id,
|
||||
'total_market_value' => $total_market_value,
|
||||
'total_cost_basis' => $performance->cost_basis,
|
||||
'total_gain' => $total_market_value - $performance->cost_basis,
|
||||
'realized_gains' => $performance->realized_gains,
|
||||
'total_dividends_earned' => $dividends_earned
|
||||
];
|
||||
if (Carbon::parse($date)->isWeekday()) {
|
||||
$daily[$date] = [
|
||||
'date' => $date,
|
||||
'portfolio_id' => $this->id,
|
||||
'total_market_value' => $total_market_value,
|
||||
'total_cost_basis' => $performance->cost_basis,
|
||||
'total_gain' => $total_market_value - $performance->cost_basis,
|
||||
'realized_gains' => $performance->realized_gains,
|
||||
'total_dividends_earned' => $dividends_earned
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
foreach ($daily as $date => $performance) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'enabled' => false
|
||||
]
|
||||
],
|
||||
'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F', ],
|
||||
'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F'],
|
||||
'stroke' => [
|
||||
'curve' => "smooth",
|
||||
'width' => 3
|
||||
|
||||
@@ -11,7 +11,7 @@ new class extends Component {
|
||||
public ?Portfolio $portfolio;
|
||||
public String $name = 'portfolio';
|
||||
public String $scope = 'YTD';
|
||||
public Array $options = [
|
||||
public Array $scopeOptions = [
|
||||
['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' => []],
|
||||
@@ -31,7 +31,7 @@ new class extends Component {
|
||||
|
||||
public function generatePerformanceData()
|
||||
{
|
||||
$filterMethod = collect($this->options)->where('id', $this->scope)->first();
|
||||
$filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first();
|
||||
|
||||
$dailyChangeQuery = DailyChange::query();
|
||||
|
||||
@@ -41,14 +41,15 @@ new class extends Component {
|
||||
|
||||
} else {
|
||||
|
||||
$dailyChangeQuery->selectRaw('date,
|
||||
$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'
|
||||
)->groupBy('date');
|
||||
SUM(total_gain) as total_gain,
|
||||
--SUM(realized_gains) as realized_gains,
|
||||
--SUM(total_dividends_earned) as total_dividends_earned
|
||||
')->groupBy('date');
|
||||
|
||||
// SUM(total_dividends_earned) as total_dividends_earned,
|
||||
// SUM(realized_gains) as realized_gains
|
||||
}
|
||||
|
||||
if ($filterMethod['method']) {
|
||||
@@ -72,10 +73,11 @@ new class extends Component {
|
||||
'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' => __('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()
|
||||
@@ -93,7 +95,7 @@ new class extends Component {
|
||||
|
||||
public function getScopeName($scope)
|
||||
{
|
||||
return collect($this->options)->where('id', $scope)->first()['name'];
|
||||
return collect($this->scopeOptions)->where('id', $scope)->first()['name'];
|
||||
}
|
||||
|
||||
}; ?>
|
||||
@@ -115,7 +117,7 @@ new class extends Component {
|
||||
|
||||
<x-dropdown title="{{ __('Choose time period') }}" label="{{ $scope }}" class="btn-ghost btn-sm" x-bind:disabled="loading">
|
||||
|
||||
@foreach($options as $option)
|
||||
@foreach($scopeOptions as $option)
|
||||
|
||||
<x-menu-item
|
||||
title="{{ $option['name'] }}"
|
||||
|
||||
@@ -21,8 +21,8 @@ class CaptureDailyChangeTest extends TestCase
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$this->portfolio = Portfolio::factory()->create();
|
||||
Transaction::factory(5)->buy()->portfolio($this->portfolio->id)->symbol('AAPL')->create();
|
||||
$this->transaction = Transaction::factory()->sell()->portfolio($this->portfolio->id)->symbol('AAPL')->create();
|
||||
Transaction::factory(5)->buy()->lastYear()->portfolio($this->portfolio->id)->symbol('AAPL')->create();
|
||||
$this->transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('AAPL')->create();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,9 +55,9 @@ class DashboardTest extends TestCase
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$portfolio = Portfolio::factory()->create();
|
||||
|
||||
Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
$transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
|
||||
Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
$transaction = Transaction::factory()->sell()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
|
||||
$metrics = Holding::query()
|
||||
->myHoldings()
|
||||
@@ -65,7 +65,7 @@ class DashboardTest extends TestCase
|
||||
->first();
|
||||
|
||||
$this->assertEqualsWithDelta(
|
||||
$transaction->sale_price - $transaction->cost_basis,
|
||||
$transaction->sale_price - $transaction->cost_basis,
|
||||
$metrics->realized_gain_dollars,
|
||||
0.01
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Tests;
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Holding;
|
||||
use Carbon\CarbonPeriod;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\DailyChange;
|
||||
use App\Models\Transaction;
|
||||
@@ -33,7 +34,7 @@ class SyncDailyChangeTest extends TestCase
|
||||
$portfolio->syncDailyChanges();
|
||||
|
||||
$count_of_daily_changes = $portfolio->daily_change()->count('date');
|
||||
$days_between_now_and_first_trans = (int) now()->diffInDays($portfolio->transactions()->min('date'), true) + 1;
|
||||
$days_between_now_and_first_trans = (int) CarbonPeriod::create($portfolio->transactions()->min('date'), now())->filter('isWeekday')->count();
|
||||
|
||||
$this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans);
|
||||
}
|
||||
@@ -49,19 +50,28 @@ class SyncDailyChangeTest extends TestCase
|
||||
$first_transaction = Transaction::factory()->buy()->yearsAgo()->portfolio($portfolio->id)->symbol('ACME')->create();
|
||||
Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]);
|
||||
$holding = Holding::symbol('ACME')->portfolio($portfolio->id)->first();
|
||||
$daily_change = DailyChange::whereDate('date', $first_transaction->date)->first();
|
||||
$daily_change = DailyChange::whereDate('date', '<=', $first_transaction->date->addDays(2))
|
||||
->whereDate('date', '>=', $first_transaction->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$this->assertEquals($holding->average_cost_basis, $daily_change->total_cost_basis);
|
||||
|
||||
$second_transaction = Transaction::factory()->buy()->lastYear()->portfolio($portfolio->id)->symbol('ACME')->create();
|
||||
Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]);
|
||||
$daily_change = DailyChange::whereDate('date', $second_transaction->date)->first();
|
||||
$daily_change = DailyChange::whereDate('date', '<=', $second_transaction->date->addDays(2))
|
||||
->whereDate('date', '>=', $second_transaction->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$this->assertEqualsWithDelta($first_transaction->cost_basis + $second_transaction->cost_basis, $daily_change->total_cost_basis, 0.01);
|
||||
|
||||
$third_transaction = Transaction::factory(2)->sell()->lastMonth()->portfolio($portfolio->id)->symbol('ACME')->create()->first();
|
||||
Artisan::call('sync:daily-change', ['portfolio_id' => $portfolio->id]);
|
||||
$daily_change = DailyChange::whereDate('date', $third_transaction->date)->first();
|
||||
$daily_change = DailyChange::whereDate('date', '<=', $third_transaction->date->addDays(2))
|
||||
->whereDate('date', '>=', $third_transaction->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$this->assertEquals(0, $daily_change->total_cost_basis);
|
||||
}
|
||||
@@ -99,12 +109,14 @@ class SyncDailyChangeTest extends TestCase
|
||||
|
||||
$this->assertEquals($day_before->realized_gains, 0);
|
||||
|
||||
$day_after = DailyChange::query()
|
||||
$after = DailyChange::query()
|
||||
->portfolio($portfolio->id)
|
||||
->whereDate('date', $sale_transaction->date->addDays(1))
|
||||
->whereDate('date', '<=', $sale_transaction->date->addDays(2))
|
||||
->whereDate('date', '>=', $sale_transaction->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$this->assertEqualsWithDelta($day_after->realized_gains, $realized_gain, 0.01);
|
||||
$this->assertEqualsWithDelta($after->realized_gains, $realized_gain, 0.01);
|
||||
}
|
||||
|
||||
public function test_dividends_captured_in_daily_change_sync(): void
|
||||
@@ -124,7 +136,9 @@ class SyncDailyChangeTest extends TestCase
|
||||
|
||||
$first_dividend_change = DailyChange::query()
|
||||
->portfolio($portfolio->id)
|
||||
->whereDate('date', $dividends->first()->date)
|
||||
->whereDate('date', '<=', $dividends->first()->date->addDays(2))
|
||||
->whereDate('date', '>=', $dividends->first()->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$owned = $dividends->first()->purchased - $dividends->first()->sold;
|
||||
@@ -133,7 +147,9 @@ class SyncDailyChangeTest extends TestCase
|
||||
|
||||
$last_dividend_change = DailyChange::query()
|
||||
->portfolio($portfolio->id)
|
||||
->whereDate('date', $dividends->last()->date)
|
||||
->whereDate('date', '<=', $dividends->last()->date->addDays(2))
|
||||
->whereDate('date', '>=', $dividends->last()->date->subDays(2))
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
$total_dividends = $dividends->reduce(function (?float $carry, $dividend) {
|
||||
|
||||
@@ -30,9 +30,9 @@ class TransactionsTest extends TestCase
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Transaction::factory(5)->buy()->symbol('AAPL')->create();
|
||||
Transaction::factory(5)->buy()->lastYear()->symbol('AAPL')->create();
|
||||
|
||||
$transaction = Transaction::factory()->sell()->symbol('AAPL')->create();
|
||||
$transaction = Transaction::factory()->sell()->lastMonth()->symbol('AAPL')->create();
|
||||
|
||||
$this->assertNotNull($transaction->cost_basis);
|
||||
}
|
||||
@@ -56,8 +56,8 @@ class TransactionsTest extends TestCase
|
||||
|
||||
$portfolio = Portfolio::factory()->create();
|
||||
|
||||
Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
$transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
$transaction = Transaction::factory()->sell()->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->create();
|
||||
|
||||
$this->assertDatabaseHas('holdings', [
|
||||
'portfolio_id' => $portfolio->id,
|
||||
|
||||
Reference in New Issue
Block a user