fix: holding calculations

This commit is contained in:
hackerESQ
2025-07-21 20:36:36 -05:00
parent 653f54add6
commit 2c3950b522
2 changed files with 96 additions and 54 deletions
+60 -34
View File
@@ -246,8 +246,6 @@ class Holding extends Model
return $query->select([ return $query->select([
'holdings.symbol', 'holdings.symbol',
'holdings.portfolio_id', 'holdings.portfolio_id',
'transactions_display.total_cost_basis',
'transactions_display.realized_gain_dollars',
'dividends_display.total_dividends_earned', 'dividends_display.total_dividends_earned',
]) ])
->groupBy([ ->groupBy([
@@ -255,8 +253,6 @@ class Holding extends Model
'holdings.quantity', 'holdings.quantity',
'holdings.portfolio_id', 'holdings.portfolio_id',
'cr.rate', 'cr.rate',
'transactions_display.total_cost_basis',
'transactions_display.realized_gain_dollars',
'dividends_display.total_dividends_earned', 'dividends_display.total_dividends_earned',
'market_data.market_value_base', 'market_data.market_value_base',
]) ])
@@ -264,10 +260,10 @@ class Holding extends Model
$join->where('cr.currency', '=', $currency); $join->where('cr.currency', '=', $currency);
if (config('database.default') === 'sqlite') { if (config('database.default') === 'sqlite') {
$join->whereRaw("strftime('%Y-%m-%d', cr.date) = ?", [
$join->whereRaw("strftime('%Y-%m-%d', cr.date) = ?", [now()->toDateString()]); now()->toDateString(),
]);
} else { } else {
$join->on('cr.date', '=', DB::raw("'".now()->toDateString()."'")); $join->on('cr.date', '=', DB::raw("'".now()->toDateString()."'"));
} }
}) })
@@ -277,16 +273,29 @@ class Holding extends Model
->selectRaw( ->selectRaw(
'holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) AS total_market_value' 'holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) AS total_market_value'
) )
->selectRaw('( ->selectRaw(
'SUM(transactions_display.realized_gain_dollars) * COALESCE(cr.rate, 1) AS realized_gain_dollars'
)
->selectRaw(
'(SUM(transactions_display.total_cost_basis) / SUM(transactions_display.total_purchases)) * holdings.quantity AS total_cost_basis'
)
->selectRaw(
'(
holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1) holdings.quantity * market_data.market_value_base * COALESCE(cr.rate, 1)
) - transactions_display.total_cost_basis as total_gain_dollars') ) - (SUM(transactions_display.total_cost_basis) / SUM(transactions_display.total_purchases)) * holdings.quantity AS total_gain_dollars'
)
->leftJoinSub( ->leftJoinSub(
DB::table('transactions') DB::table('transactions')
->leftJoin('currency_rates as cr', function ($join) use ($currency) { ->leftJoin('currency_rates as cr', function ($join) use ($currency) {
$join->on('cr.date', '=', 'transactions.date') $join
->on('cr.date', '=', 'transactions.date')
->where('cr.currency', '=', $currency); ->where('cr.currency', '=', $currency);
}) })
->select(['transactions.symbol', 'transactions.portfolio_id']) ->select([
'transactions.id',
'transactions.symbol',
'transactions.portfolio_id',
])
->leftJoinSub( ->leftJoinSub(
DB::table('transactions') DB::table('transactions')
->leftJoin('currency_rates as cr', function ($join) use ($currency) { ->leftJoin('currency_rates as cr', function ($join) use ($currency) {
@@ -298,10 +307,11 @@ class Holding extends Model
'transactions.symbol', 'transactions.symbol',
'transactions.portfolio_id', 'transactions.portfolio_id',
'transactions.quantity', 'transactions.quantity',
'transactions.cost_basis_base',
'transactions.date', 'transactions.date',
]) ])
->selectRaw( ->selectRaw("
"(CASE (CASE
WHEN WHEN
transactions.transaction_type = 'BUY' transactions.transaction_type = 'BUY'
OR SUM(transactions.cost_basis_base) = 0 OR SUM(transactions.cost_basis_base) = 0
@@ -320,49 +330,64 @@ class Holding extends Model
AND buy.transaction_type = 'BUY' AND buy.transaction_type = 'BUY'
AND buy.date <= transactions.date AND buy.date <= transactions.date
) END) ) END)
AS rate" AS rate")
)
->selectRaw(
"CASE
WHEN transactions.transaction_type = 'BUY'
THEN transactions.quantity
ELSE -transactions.quantity
END
AS remaining_quantity"
)
->groupBy([ ->groupBy([
'transactions.id',
'transactions.symbol', 'transactions.symbol',
'transactions.date', 'transactions.date',
'transactions.portfolio_id', 'transactions.portfolio_id',
'transactions.transaction_type', 'transactions.transaction_type',
'transactions.cost_basis_base',
'transactions.quantity', 'transactions.quantity',
'cr.rate', 'cr.rate',
]), 'cost_basis_display', function ($join) { ]),
$join->on('transactions.symbol', '=', 'cost_basis_display.symbol') 'cost_basis_display',
->on('transactions.portfolio_id', '=', 'cost_basis_display.portfolio_id') function ($join) {
$join
->on('transactions.symbol', '=', 'cost_basis_display.symbol')
->on(
'transactions.portfolio_id',
'=',
'cost_basis_display.portfolio_id'
)
->on('transactions.date', '=', 'cost_basis_display.date'); ->on('transactions.date', '=', 'cost_basis_display.date');
}) }
->selectRaw(
"SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN (transactions.sale_price_base - transactions.cost_basis_base) * transactions.quantity * COALESCE(cr.rate, 1) ELSE 0 END) AS realized_gain_dollars"
) )
->selectRaw( ->selectRaw(
"SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.cost_basis_base * transactions.quantity * cost_basis_display.rate END) / SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity END) * SUM(cost_basis_display.remaining_quantity) AS total_cost_basis" "CASE WHEN transactions.transaction_type = 'SELL' THEN (transactions.sale_price_base - transactions.cost_basis_base) * transactions.quantity * COALESCE(cr.rate, 1) END AS realized_gain_dollars"
) )
->groupBy(['transactions.symbol', 'transactions.portfolio_id']), ->selectRaw(
"CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.cost_basis_base * transactions.quantity * cost_basis_display.rate END AS total_cost_basis"
)
->selectRaw(
"CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity END AS total_purchases"
)
->groupBy([
'transactions.id',
'transactions.symbol',
'transactions.portfolio_id',
'transactions.cost_basis_base',
'transactions.quantity',
'cost_basis_display.rate',
'cr.rate',
]),
'transactions_display', 'transactions_display',
function ($join) { function ($join) {
$join->on('holdings.symbol', '=', 'transactions_display.symbol') $join
->on('holdings.symbol', '=', 'transactions_display.symbol')
->on('holdings.portfolio_id', '=', 'transactions_display.portfolio_id'); ->on('holdings.portfolio_id', '=', 'transactions_display.portfolio_id');
} }
) )
->leftJoinSub( ->leftJoinSub(
DB::table('dividends') DB::table('dividends')
->join('transactions as tx', function ($join) { ->join('transactions as tx', function ($join) {
$join->on('tx.symbol', '=', 'dividends.symbol') $join
->on('tx.symbol', '=', 'dividends.symbol')
->on('tx.date', '<=', 'dividends.date'); ->on('tx.date', '<=', 'dividends.date');
}) })
->leftJoin('currency_rates as cr', function ($join) use ($currency) { ->leftJoin('currency_rates as cr', function ($join) use ($currency) {
$join->on('cr.date', '=', 'dividends.date') $join
->on('cr.date', '=', 'dividends.date')
->where('cr.currency', '=', $currency); ->where('cr.currency', '=', $currency);
}) })
->select(['dividends.symbol']) ->select(['dividends.symbol'])
@@ -375,6 +400,7 @@ class Holding extends Model
$join->on('holdings.symbol', '=', 'dividends_display.symbol'); $join->on('holdings.symbol', '=', 'dividends_display.symbol');
} }
); );
} }
public function syncTransactionsAndDividends() public function syncTransactionsAndDividends()
+16
View File
@@ -43,4 +43,20 @@ class HoldingsTest extends TestCase
$holding = Holding::query()->getPortfolioMetrics(); $holding = Holding::query()->getPortfolioMetrics();
$this->assertEquals(0, $holding->get('total_cost_basis')); $this->assertEquals(0, $holding->get('total_cost_basis'));
} }
public function test_calculates_cost_bases_on_same_day_buy_sell_transaction(): void
{
$this->actingAs($user = User::factory()->create());
$portfolio = Portfolio::factory()->create();
Transaction::factory(2)->buy()->lastYear()->costBasis(100)->portfolio($portfolio->id)->symbol('AAPL')->create();
Transaction::factory(2)->buy()->lastYear()->costBasis(300)->portfolio($portfolio->id)->symbol('AAPL')->create();
Transaction::factory()->sell()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create();
Transaction::factory()->sell()->recent()->portfolio($portfolio->id)->symbol('AAPL')->create();
$holding = Holding::query()->getPortfolioMetrics();
$this->assertEquals(400, $holding->get('total_cost_basis'));
}
} }