improve sync daily changes command

This commit is contained in:
hackerESQ
2024-09-15 11:28:13 -05:00
parent 9708716c71
commit 9696339828
5 changed files with 73 additions and 16 deletions
+3 -1
View File
@@ -64,9 +64,11 @@ class SyncDailyChange extends Command implements PromptsForMissingInput
$portfolio = Portfolio::findOrFail($this->argument('portfolio_id')); $portfolio = Portfolio::findOrFail($this->argument('portfolio_id'));
$this->output->write('Syncing daily change history... This may take a moment.', false);
$portfolio->syncDailyChanges(); $portfolio->syncDailyChanges();
$this->line('Awesome! Daily change history for '. $portfolio->title .' has been completed.'); $this->output->write('Awesome! Daily change history for '. $portfolio->title .' has been completed.', false);
} catch (\Throwable $e) { } catch (\Throwable $e) {
+12 -2
View File
@@ -8,7 +8,6 @@ use App\Models\Portfolio;
use App\Models\MarketData; use App\Models\MarketData;
use App\Models\Transaction; use App\Models\Transaction;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -255,7 +254,18 @@ class Holding extends Model
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0) AS `owned` COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0) AS `owned`
"), "),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'BUY' THEN (quantity * cost_basis) ELSE 0 END), 0) AS `cost_basis`"), DB::raw("
COALESCE(CASE
WHEN (
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN transactions.transaction_type = 'SELL' THEN transactions.quantity ELSE 0 END), 0)
) = 0 THEN 0
ELSE SUM(CASE
WHEN transactions.transaction_type = 'BUY' THEN transactions.quantity * transactions.cost_basis
ELSE 0
END)
END, 0) AS cost_basis
"),
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`") DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`")
]) ])
->leftJoin('transactions', function ($join) { ->leftJoin('transactions', function ($join) {
+16 -13
View File
@@ -102,40 +102,43 @@ class Portfolio extends Model
->groupBy(['holdings.symbol', 'holdings.portfolio_id']) ->groupBy(['holdings.symbol', 'holdings.portfolio_id'])
->get(); ->get();
$dividends = Dividend::whereIn('symbol', $holdings->pluck('symbol'))->get();
$total_performance = []; $total_performance = [];
$holdings->each(function($holding) use (&$total_performance) { $holdings->each(function($holding) use (&$total_performance, $dividends) {
$holding->setRelation('dividends', $dividends->where('symbol', $holding->symbol));
$all_history = app(MarketDataInterface::class)->history($holding->symbol, $holding->first_transaction_date, now()); $all_history = app(MarketDataInterface::class)->history($holding->symbol, $holding->first_transaction_date, now());
$quantity = $holding->dailyPerformance($holding->first_transaction_date, now()); $daily_performance = $holding->dailyPerformance($holding->first_transaction_date, now());
$dividends = $holding->dividends->keyBy(function ($dividend, $key) { $dividends = $holding->dividends->keyBy(function ($dividend, $key) {
return $dividend['date']->format('Y-m-d'); return $dividend['date']->format('Y-m-d');
})->toArray(); });
$dividends_earned = 0; $dividends_earned = 0;
$daily_performance = []; $daily = [];
$all_history->sortBy('date')->each(function ($history, $date) use ($quantity, $dividends, &$daily_performance, &$dividends_earned) { $all_history->sortBy('date')->each(function ($history, $date) use ($daily_performance, $dividends, &$daily, &$dividends_earned) {
$close = Arr::get($history, 'close', 0); $close = Arr::get($history, 'close', 0);
$total_market_value = $quantity->get($date)->owned * $close; $total_market_value = $daily_performance->get($date)->owned * $close;
$dividend = Arr::get($dividends, $date, []); $dividends_earned += $daily_performance->get($date)->owned * ($dividends->get($date)?->dividend_amount ?? 0);
$dividends_earned += $quantity->get($date)->owned * Arr::get($dividend, 'dividend_amount', 0);
$daily_performance[$date] = [ $daily[$date] = [
'date' => $date, 'date' => $date,
'portfolio_id' => $this->id, 'portfolio_id' => $this->id,
'total_market_value' => $total_market_value, 'total_market_value' => $total_market_value,
'total_cost_basis' => $quantity->get($date)->cost_basis, 'total_cost_basis' => $daily_performance->get($date)->cost_basis,
'total_gain' => $total_market_value - $quantity->get($date)->cost_basis, 'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis,
'realized_gains' => $quantity->get($date)->realized_gains, 'realized_gains' => $daily_performance->get($date)->realized_gains,
'total_dividends_earned' => $dividends_earned 'total_dividends_earned' => $dividends_earned
]; ];
}); });
foreach ($daily_performance as $date => $performance) { foreach ($daily as $date => $performance) {
if (!isset($total_performance[$date])) { if (!isset($total_performance[$date])) {
$total_performance[$date] = $performance; $total_performance[$date] = $performance;
+14
View File
@@ -43,6 +43,13 @@ class TransactionFactory extends Factory
]); ]);
} }
public function lastYear(): static
{
return $this->state(fn (array $attributes) => [
'date' => now()->subYear()->format('Y-m-d'),
]);
}
public function lastMonth(): static public function lastMonth(): static
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
@@ -50,6 +57,13 @@ class TransactionFactory extends Factory
]); ]);
} }
public function recent(): static
{
return $this->state(fn (array $attributes) => [
'date' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d'),
]);
}
public function symbol($symbol): static public function symbol($symbol): static
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
+28
View File
@@ -38,6 +38,34 @@ class SyncDailyChangeTest extends TestCase
$this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans); $this->assertEquals($count_of_daily_changes, $days_between_now_and_first_trans);
} }
/**
*/
public function test_cost_basis_is_synced(): void
{
$this->actingAs($user = User::factory()->create());
$portfolio = Portfolio::factory()->create();
$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();
$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();
$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();
$this->assertEquals(0, $daily_change->total_cost_basis);
}
/** /**
*/ */
public function test_sales_are_captured_as_realized_gains(): void public function test_sales_are_captured_as_realized_gains(): void