Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e492475c0 | |||
| c454e85ad4 | |||
| 487322abb5 | |||
| f78c521dc4 | |||
| ff9bcd782f | |||
| 1ccf515ca2 | |||
| 1b0f9c134c | |||
| 3589242996 |
@@ -111,7 +111,7 @@ class CurrencyRate extends Model
|
||||
*
|
||||
* @return array<string, float>
|
||||
*/
|
||||
public static function timeSeriesRates(?string $currency = null, 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,6 +132,16 @@ class CurrencyRate extends Model
|
||||
return $dateRange;
|
||||
}
|
||||
|
||||
if (is_array($currency)) {
|
||||
|
||||
foreach ($currency as $curr) {
|
||||
|
||||
dispatch(fn () => self::timeSeriesRates($curr, $start, $end));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// handle currency alias
|
||||
if (! empty($currency)) {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Transaction;
|
||||
use Database\Seeders\CurrencySeeder;
|
||||
use Database\Seeders\MarketDataSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -24,10 +25,15 @@ return new class extends Migration
|
||||
* Add options column to users table
|
||||
*/
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->json('options')->default(json_encode([
|
||||
'locale' => config('app.locale', 'en'),
|
||||
'display_currency' => config('investbrain.base_currency', 'USD'),
|
||||
]))->after('profile_photo_path');
|
||||
|
||||
$locale = config('app.locale', 'en');
|
||||
$currency = config('investbrain.base_currency', 'USD');
|
||||
|
||||
$default = config('database.default') === 'mysql'
|
||||
? new Expression("(JSON_OBJECT('locale', '{$locale}', 'display_currency', '{$currency}'))")
|
||||
: json_encode(['locale' => $locale, 'display_currency' => $currency]);
|
||||
|
||||
$table->json('options')->default($default)->after('profile_photo_path');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -97,22 +103,17 @@ return new class extends Migration
|
||||
'--force' => true,
|
||||
]);
|
||||
|
||||
Holding::all()->groupBy('market_data.currency')->keys()->each(
|
||||
fn ($currency) => dispatch(
|
||||
function () use ($currency) {
|
||||
CurrencyRate::timeSeriesRates(
|
||||
$currency,
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,15 +52,17 @@ class MarketDataSeeder extends Seeder
|
||||
$rowCount++;
|
||||
|
||||
if ($rowCount % $chunkSize == 0) {
|
||||
$this->bulkInsert($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
|
||||
@@ -76,19 +76,17 @@ class MarketDataSeeder extends Seeder
|
||||
}
|
||||
}
|
||||
|
||||
public function bulkInsert($rows)
|
||||
private function bulkInsert($rows): void
|
||||
{
|
||||
try {
|
||||
|
||||
dispatch(
|
||||
fn () => DB::table('market_data')->upsert($rows, ['symbol'], ['name', 'currency', 'meta_data'])
|
||||
);
|
||||
|
||||
$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
@@ -62,7 +62,7 @@ RUN apk add --no-cache \
|
||||
bash \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
gd pgsql zip pdo_mysql mysqli intl
|
||||
gd pgsql zip pdo_mysql pdo_pgsql mysqli intl
|
||||
|
||||
# Remove default nginx config
|
||||
RUN rm -rf /var/www/html \
|
||||
|
||||
@@ -221,7 +221,7 @@ class DailyChangeTest extends TestCase
|
||||
$second_transaction = Transaction::create([
|
||||
'symbol' => 'AAPL',
|
||||
'portfolio_id' => $portfolio->id,
|
||||
'date' => now()->subYears(3),
|
||||
'date' => now()->subDays(1080), // 3 years
|
||||
'quantity' => 1,
|
||||
'cost_basis' => 39.89,
|
||||
'transaction_type' => 'BUY',
|
||||
|
||||
@@ -21,7 +21,7 @@ class MarketDataTest extends TestCase
|
||||
'--force' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(14262, MarketData::count('symbol'));
|
||||
$this->assertEquals(13187, MarketData::count('symbol'));
|
||||
}
|
||||
|
||||
public function test_can_get_quote_from_provider()
|
||||
|
||||
@@ -261,6 +261,37 @@ class MultiCurrencyTest extends TestCase
|
||||
$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()
|
||||
{
|
||||
|
||||
|
||||
Reference in New Issue
Block a user