Fix multi-currency imports (#94)
This commit is contained in:
+36
-27
@@ -111,7 +111,7 @@ class CurrencyRate extends Model
|
||||
*
|
||||
* @return array<string, float>
|
||||
*/
|
||||
public static function timeSeriesRates(string $currency, mixed $start = null, mixed $end = null): array
|
||||
public static function timeSeriesRates(string|array $currency, mixed $start = null, mixed $end = null): array
|
||||
{
|
||||
if (empty($start)) {
|
||||
return [];
|
||||
@@ -134,22 +134,27 @@ class CurrencyRate extends Model
|
||||
|
||||
[$currency, $adjustment] = self::getCurrencyAliasAdjustments($currency);
|
||||
|
||||
$currencies = Currency::all()->pluck('currency')->toArray();
|
||||
if (! empty($currency)) {
|
||||
|
||||
// call api in chunks
|
||||
$rates = [];
|
||||
foreach (collect($period)->chunk(500) as $chunk) {
|
||||
$currencies = Arr::wrap($currency);
|
||||
|
||||
$chunkRates = Frankfurter::setSymbols($currencies)->timeSeries($chunk->min(), $chunk->max());
|
||||
} else {
|
||||
|
||||
$rates = array_merge($rates, Arr::get($chunkRates, 'rates', []));
|
||||
$currencies = Currency::all()->pluck('currency')->toArray();
|
||||
}
|
||||
|
||||
// get rates
|
||||
$rates = Frankfurter::setSymbols($currencies)->timeSeries($period->first(), $period->last());
|
||||
|
||||
$rates = collect(Arr::get($rates, 'rates', []))->sortKeys()->toArray();
|
||||
|
||||
$datesOnly = array_keys($rates);
|
||||
|
||||
// loop through each date
|
||||
$updates = [];
|
||||
foreach ($period as $date) {
|
||||
|
||||
$lookupDate = self::getNearestPastDate($date, $rates);
|
||||
$lookupDate = self::getNearestPastDate($date, $datesOnly, $rates);
|
||||
|
||||
if (is_null($lookupDate)) {
|
||||
continue;
|
||||
@@ -181,33 +186,39 @@ class CurrencyRate extends Model
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private static function getNearestPastDate(CarbonInterface $date, array $rates): ?CarbonInterface
|
||||
private static function getNearestPastDate(CarbonInterface $date, array $datesOnly, array $rates): ?CarbonInterface
|
||||
{
|
||||
$datesWithRates = array_keys($rates);
|
||||
sort($datesWithRates);
|
||||
|
||||
// if no dates, nothing to do...
|
||||
if (empty($datesOnly)) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$mutableDate = $date->copy();
|
||||
$weekAgo = $date->copy()->subWeek();
|
||||
$firstDate = Carbon::parse($datesOnly[0]);
|
||||
|
||||
// get rates or find closest valid rate (handles missing weekend rates)
|
||||
while (! isset($rates[$date->toDateString()])) {
|
||||
while (! isset($rates[$mutableDate->toDateString()])) {
|
||||
|
||||
// prevent runaway infinite loops
|
||||
if ($mutableDate->lessThan($weekAgo)) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// is this the start of a range that falls on a weekend?
|
||||
if ($date->lessThan($first_date = Carbon::parse($datesWithRates[0]))) {
|
||||
if ($mutableDate->lessThan($firstDate)) {
|
||||
|
||||
$date = $first_date;
|
||||
break;
|
||||
return $firstDate;
|
||||
}
|
||||
|
||||
// try the day before then
|
||||
$date = Carbon::parse($date)->subDay();
|
||||
|
||||
// prevent runaway infinite loops
|
||||
if ($date->lessThan($date->copy()->subWeek())) {
|
||||
|
||||
$date = null;
|
||||
break;
|
||||
}
|
||||
$mutableDate = $mutableDate->subDay();
|
||||
}
|
||||
|
||||
return $date;
|
||||
return $mutableDate;
|
||||
}
|
||||
|
||||
public static function refreshCurrencyData($force = false): void
|
||||
@@ -248,9 +259,7 @@ class CurrencyRate extends Model
|
||||
public static function chunkInsert(array $updates): void
|
||||
{
|
||||
|
||||
$chunks = array_chunk($updates, 500);
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
foreach (array_chunk($updates, 500) as $chunk) {
|
||||
|
||||
QueuedCurrencyRateInsertJob::dispatch($chunk);
|
||||
}
|
||||
|
||||
+16
-10
@@ -105,20 +105,25 @@ class Dividend extends Model
|
||||
|
||||
$market_data = MarketData::getMarketData($symbol);
|
||||
|
||||
// get historic conversion rates
|
||||
$rate_to_base = CurrencyRate::timeSeriesRates($market_data->currency, $start_date, $end_date);
|
||||
$dividend_data
|
||||
->chunk(10)
|
||||
->each(function ($chunk) use ($market_data) {
|
||||
|
||||
// create mass insert
|
||||
foreach ($dividend_data as $index => $dividend) {
|
||||
$rate_to_base_date = 1 / Arr::get($rate_to_base, Carbon::parse(Arr::get($dividend, 'date'))->toDateString(), 1);
|
||||
// get historic conversion rates
|
||||
$rate_to_base = CurrencyRate::timeSeriesRates($market_data->currency, $chunk->min('date'), $chunk->max('date'));
|
||||
|
||||
$dividend['dividend_amount_base'] = $dividend['dividend_amount'] * $rate_to_base_date;
|
||||
// create mass insert
|
||||
foreach ($chunk as $index => $dividend) {
|
||||
$rate_to_base_date = 1 / Arr::get($rate_to_base, Carbon::parse(Arr::get($dividend, 'date'))->toDateString(), 1);
|
||||
|
||||
$dividend_data[$index] = [...$dividend, ...['id' => Str::uuid()->toString(), 'updated_at' => now(), 'created_at' => now()]];
|
||||
}
|
||||
$dividend['dividend_amount_base'] = $dividend['dividend_amount'] * $rate_to_base_date;
|
||||
|
||||
// insert records
|
||||
(new self)->insertOrIgnore($dividend_data->toArray());
|
||||
$chunk[$index] = [...$dividend, ...['id' => Str::uuid()->toString(), 'updated_at' => now(), 'created_at' => now()]];
|
||||
}
|
||||
|
||||
// insert records
|
||||
(new self)->insertOrIgnore($chunk->toArray());
|
||||
});
|
||||
|
||||
// sync to holdings
|
||||
self::syncHoldings($symbol);
|
||||
@@ -186,6 +191,7 @@ class Dividend extends Model
|
||||
'date' => $dividend['date'],
|
||||
'portfolio_id' => $holding->portfolio_id,
|
||||
'symbol' => $holding->symbol,
|
||||
'currency' => $holding->market_data->currency,
|
||||
'transaction_type' => 'BUY',
|
||||
'reinvested_dividend' => true,
|
||||
'cost_basis' => 0,
|
||||
|
||||
@@ -152,7 +152,12 @@ class Portfolio extends Model
|
||||
|
||||
$total_performance = [];
|
||||
|
||||
$holdings->each(function ($holding) use (&$total_performance) {
|
||||
// get unique currencies for holdings
|
||||
foreach ($holdings->groupBy('market_data.currency')->keys() as $currency) {
|
||||
$currency_rates[$currency] = CurrencyRate::timeSeriesRates($currency, $holdings->min('first_transaction_date'), now());
|
||||
}
|
||||
|
||||
$holdings->each(function ($holding) use (&$total_performance, $currency_rates) {
|
||||
|
||||
$period = CarbonPeriod::create(
|
||||
$holding->first_transaction_date,
|
||||
@@ -163,7 +168,6 @@ class Portfolio extends Model
|
||||
|
||||
$daily_performance = $holding->dailyPerformance($holding->first_transaction_date, now());
|
||||
$all_history = app(MarketDataInterface::class)->history($holding->symbol, $holding->first_transaction_date, now());
|
||||
$currency_rates = CurrencyRate::timeSeriesRates($holding->market_data->currency, $holding->first_transaction_date, now());
|
||||
|
||||
$holding_performance = [];
|
||||
|
||||
@@ -179,7 +183,7 @@ class Portfolio extends Model
|
||||
$holding_performance[$date] = [
|
||||
'date' => $date,
|
||||
'portfolio_id' => $this->id,
|
||||
'total_market_value' => $total_market_value * (1 / Arr::get($currency_rates, $date, 1)),
|
||||
'total_market_value' => $total_market_value * (1 / Arr::get($currency_rates[$holding->market_data->currency], $date, 1)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user