Compare commits

..

7 Commits

Author SHA1 Message Date
hackerESQ f78c521dc4 fix: add bp.l to test multicurr seed 2025-05-16 21:12:48 -05:00
hackerESQ ff9bcd782f fix: don't queue market data seed 2025-05-16 20:49:29 -05:00
hackerESQ 1ccf515ca2 fix: reorg migrtion 2025-05-16 19:59:39 -05:00
hackerESQ 1b0f9c134c fix: dispatch time series rates 2025-05-16 19:38:58 -05:00
hackerESQ 3589242996 fix: dispatch time series updates 2025-05-16 19:31:44 -05:00
hackerESQ 689aa4d50b fix: multi currency seeders 2025-05-15 20:05:14 -05:00
hackerESQ 26370c03c4 fix: optimize migration to multi-currency 2025-05-03 13:22:45 -05:00
6 changed files with 111 additions and 1310 deletions
+27 -13
View File
@@ -111,7 +111,7 @@ class CurrencyRate extends Model
*
* @return array<string, float>
*/
public static function timeSeriesRates(string|array $currency, mixed $start = null, mixed $end = null): array
public static function timeSeriesRates(string|array|null $currency = null, mixed $start = null, mixed $end = null): array
{
if (empty($start)) {
return [];
@@ -132,19 +132,28 @@ class CurrencyRate extends Model
return $dateRange;
}
[$currency, $adjustment] = self::getCurrencyAliasAdjustments($currency);
if (is_array($currency)) {
foreach ($currency as $curr) {
dispatch(fn () => self::timeSeriesRates($curr, $start, $end));
}
return [];
}
// handle currency alias
if (! empty($currency)) {
$currencies = Arr::wrap($currency);
[$currency, $adjustment] = self::getCurrencyAliasAdjustments($currency);
} else {
$currencies = Currency::all()->pluck('currency')->toArray();
$currency = Currency::all()->pluck('currency')->toArray();
}
// get rates
$rates = Frankfurter::setSymbols($currencies)->timeSeries($period->first(), $period->last());
$rates = Frankfurter::setSymbols($currency)->timeSeries($period->first(), $period->last());
$rates = collect(Arr::get($rates, 'rates', []))->sortKeys()->toArray();
@@ -177,13 +186,18 @@ class CurrencyRate extends Model
// persist
self::chunkInsert($updates);
return collect($updates)
->whereBetween('date', [$start, $end ?? now()])
->where('currency', $currency)
->mapWithKeys(fn ($rate) => [
$rate['date'] => $rate['rate'] * $adjustment,
])
->toArray();
if (is_string($currency)) {
return collect($updates)
->whereBetween('date', [$start, $end ?? now()])
->where('currency', $currency)
->mapWithKeys(fn ($rate) => [
$rate['date'] => $rate['rate'] * ($adjustment ?? 1),
])
->toArray();
}
return [];
}
private static function getNearestPastDate(CarbonInterface $date, array $datesOnly, array $rates): ?CarbonInterface
@@ -265,7 +279,7 @@ class CurrencyRate extends Model
}
}
protected static function getCurrencyAliasAdjustments($currency)
protected static function getCurrencyAliasAdjustments(string $currency)
{
$adjustment = 1;
@@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Models\CurrencyRate;
use App\Models\Holding;
use App\Models\Transaction;
use Database\Seeders\CurrencySeeder;
use Database\Seeders\MarketDataSeeder;
@@ -96,17 +97,17 @@ return new class extends Migration
'--force' => true,
]);
CurrencyRate::timeSeriesRates(
'', // use fake currency to force
Transaction::min('date')
);
CurrencyRate::refreshCurrencyData();
Artisan::call('db:seed', [
'--class' => MarketDataSeeder::class,
'--force' => true,
]);
CurrencyRate::timeSeriesRates(
Holding::all()->groupBy('market_data.currency')->keys()->toArray(),
Transaction::min('date')
);
CurrencyRate::refreshCurrencyData();
}
/**
+10 -10
View File
@@ -12,8 +12,6 @@ class MarketDataSeeder extends Seeder
{
use WithoutModelEvents;
public array $rows = [];
/**
* Run the database seeds.
*/
@@ -44,7 +42,7 @@ class MarketDataSeeder extends Seeder
$meta_data = json_decode(base64_decode($data['meta_data']), true);
$meta_data['source'] = 'market_data_seeder';
$this->rows[] = [
$rows[] = [
'symbol' => $data['symbol'],
'name' => $data['name'],
'currency' => $data['currency'],
@@ -54,16 +52,17 @@ class MarketDataSeeder extends Seeder
$rowCount++;
if ($rowCount % $chunkSize == 0) {
DB::table('market_data')->upsert($this->rows, ['symbol'], ['name', 'currency', 'meta_data']);
$this->rows = [];
$this->bulkInsert($rows);
$rows = [];
}
}
}
// final clean up
if (! empty($this->rows)) {
if (! empty($rows)) {
$this->bulkInsert($this->rows);
$this->bulkInsert($rows);
$rows = [];
}
// Close the CSV file
@@ -77,16 +76,17 @@ class MarketDataSeeder extends Seeder
}
}
public function bulkInsert(array $rows)
private function bulkInsert($rows): void
{
try {
DB::table('market_data')->insertOrIgnore($rows);
$this->rows = [];
DB::table('market_data')->upsert($rows, ['symbol'], ['name', 'currency', 'meta_data']);
} catch (\Throwable $e) {
throw new \Exception('Error: '.$e->getMessage());
}
gc_collect_cycles();
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -21,7 +21,7 @@ class MarketDataTest extends TestCase
'--force' => true,
]);
$this->assertEquals(14464, MarketData::count('symbol'));
$this->assertEquals(13187, MarketData::count('symbol'));
}
public function test_can_get_quote_from_provider()
+64 -1
View File
@@ -225,8 +225,71 @@ class MultiCurrencyTest extends TestCase
->andReturn(['rates' => $results]);
$result = CurrencyRate::timeSeriesRates('ZZZ', $start, $end);
$this->assertEquals(count($period) - 1, count($result));
$result = CurrencyRate::all();
$this->assertEquals(count($period), count($result));
}
public function test_can_get_time_series_rates_with_null_currency()
{
$start = now()->subWeeks(2);
$end = now();
$period = CarbonPeriod::create($start, $end);
// mock response from Frankfurter
$results = [];
collect($period->copy()->filter('isWeekday'))->each(function ($date) use (&$results) {
$date = $date->toDateString();
$results[$date] = [
'FOO' => random_int(10, 150) / 1000,
];
});
Frankfurter::expects('setSymbols')
->andReturnSelf();
Frankfurter::expects('timeSeries')
->andReturn(['rates' => $results]);
$result = CurrencyRate::timeSeriesRates(null, $start, $end);
$this->assertEquals(0, count($result));
$result = CurrencyRate::all();
$this->assertEquals(count($period), count($result));
}
public function test_can_get_time_series_rates_with_currencies()
{
$start = now()->subWeeks(2);
$end = now();
$period = CarbonPeriod::create($start, $end);
// mock response from Frankfurter
$results = [];
collect($period->copy()->filter('isWeekday'))->each(function ($date) use (&$results) {
$date = $date->toDateString();
$results[$date] = [
'FOO' => random_int(10, 150) / 1000,
'BAR' => random_int(10, 150) / 1000,
];
});
Frankfurter::expects('setSymbols')
->andReturnSelf();
Frankfurter::expects('timeSeries')
->andReturn(['rates' => $results]);
$result = CurrencyRate::timeSeriesRates(null, $start, $end);
$this->assertEquals(0, count($result));
$result = CurrencyRate::all();
$this->assertEquals(count($period) * 2, count($result));
}
public function test_time_series_rate_calls_are_chunked()