Compare commits

...

22 Commits

Author SHA1 Message Date
hackerESQ c691ee922a wip test 2025-04-12 21:25:00 -05:00
hackerESQ 3eb9bad840 www 2025-04-12 20:39:12 -05:00
hackerESQ 370f7bb54b wip 2025-04-12 20:31:49 -05:00
hackerESQ 62bf6797e6 wip 2025-04-12 20:29:28 -05:00
hackerESQ c4e3645145 wip 2025-04-12 20:27:06 -05:00
hackerESQ 69e4d0fb3a wip 2025-04-12 20:22:43 -05:00
hackerESQ 20c2cb37cc wip 2025-04-12 20:16:34 -05:00
hackerESQ d2bb065822 wip 2025-04-12 20:11:05 -05:00
hackerESQ 0c00f28d97 wip 2025-04-12 20:06:21 -05:00
hackerESQ 5eab00ee33 wip 2025-04-12 20:03:40 -05:00
hackerESQ 56064ad84e try again 2025-04-12 18:02:23 -05:00
hackerESQ c96ff0e45f wip 2025-04-12 17:51:13 -05:00
hackerESQ 33e0df5ae2 wip 2025-04-12 17:37:26 -05:00
hackerESQ a5a333f784 wip 2025-04-12 17:31:28 -05:00
hackerESQ 89b5505e1d wip 2025-04-12 17:27:38 -05:00
hackerESQ 60923b3c93 wip 2025-04-12 17:16:36 -05:00
hackerESQ 17e5d8b665 fix: increase chunk size 2025-04-12 10:12:30 -05:00
hackerESQ bd9c828c68 fix: use options prop 2025-04-11 21:49:06 -05:00
hackerESQ f72cd6f5a7 fix: set name attribute 2025-04-11 21:45:58 -05:00
hackerESQ 3593697cce fix: user needs to be set from import job 2025-04-11 21:42:38 -05:00
hackerESQ d53e71dcd5 Update README.md 2025-04-11 21:28:05 -05:00
hackerESQ 71e79cfb40 fix: daily change should be synced when before latest transaction 2025-04-11 21:14:53 -05:00
7 changed files with 79 additions and 38 deletions
+1
View File
@@ -193,6 +193,7 @@ Just to be safe, we recommend backing up your portfolios before using these comm
| refresh:market-data | Refreshes market data with your configured market data provider. | | refresh:market-data | Refreshes market data with your configured market data provider. |
| refresh:dividend-data | Refreshes dividend data with your configured market data provider. Will also re-calculate your total dividends earned for each holding. | | refresh:dividend-data | Refreshes dividend data with your configured market data provider. Will also re-calculate your total dividends earned for each holding. |
| refresh:split-data | Refreshes splits data with your configured market data provider. Will also create new transactions to account for any splits. | | refresh:split-data | Refreshes splits data with your configured market data provider. Will also create new transactions to account for any splits. |
| refresh:currency-data | Grabs the latest daily currency exchange rate data and persists to the database. |
| capture:daily-change | Captures a snapshot of each portfolio's daily performance. | | capture:daily-change | Captures a snapshot of each portfolio's daily performance. |
| sync:daily-change | Re-calculates daily snapshots of your portfolio's daily performance. Useful to fill in gaps in your portfolio charts. (Note: this is an extremely resource intensive query.) | | sync:daily-change | Re-calculates daily snapshots of your portfolio's daily performance. Useful to fill in gaps in your portfolio charts. (Note: this is an extremely resource intensive query.) |
| sync:holdings | Re-calculates performance of holdings with related transactions (i.e. dividends, realized gains, etc). | | sync:holdings | Re-calculates performance of holdings with related transactions (i.e. dividends, realized gains, etc). |
+1 -1
View File
@@ -21,7 +21,7 @@ class EnsureDailyChangeIsSynced
! Cache::has($cacheKey) ! Cache::has($cacheKey)
&& $model->date->lessThan(now()) && $model->date->lessThan(now())
&& ($model->date->lessThan($model->portfolio->daily_change()->min('date') ?? now()) && ($model->date->lessThan($model->portfolio->daily_change()->min('date') ?? now())
|| $model->date->lessThan($model->portfolio->transactions()->where('id', '!=', $model->id)->min('date') ?? now()) || $model->date->lessThan($model->portfolio->transactions()->where('id', '!=', $model->id)->max('date') ?? now())
) )
) { ) {
defer(fn () => $model->portfolio->syncDailyChanges()); defer(fn () => $model->portfolio->syncDailyChanges());
+6 -8
View File
@@ -36,24 +36,22 @@ class ConfigSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadi
public function collection(Collection $configs) public function collection(Collection $configs)
{ {
$user = auth()->user();
foreach ($configs as $config) { foreach ($configs as $config) {
switch ($config['key']) { switch ($config['key']) {
case 'name': case 'name':
$user->name = $config['value']; $this->backupImport->user->setAttribute('name', $config['value']);
$user->save(); $this->backupImport->user->save();
break; break;
case 'locale': case 'locale':
$user->setOption('locale', $config['value']); $this->backupImport->user->setOption('locale', $config['value']);
$user->save(); $this->backupImport->user->save();
break; break;
case 'display_currency': case 'display_currency':
$user->setOption('display_currency', $config['value']); $this->backupImport->user->setOption('display_currency', $config['value']);
$user->save(); $this->backupImport->user->save();
break; break;
case 'reinvest_dividends': case 'reinvest_dividends':
+5
View File
@@ -50,4 +50,9 @@ class BackupImport extends Model
'completed_at' => 'datetime', 'completed_at' => 'datetime',
]; ];
} }
public function user()
{
return $this->belongsTo(User::class);
}
} }
+40 -23
View File
@@ -119,58 +119,74 @@ class CurrencyRate extends Model
$end = $end ?? now(); $end = $end ?? now();
dump('Creating period');
$period = CarbonPeriod::create($start, $end); $period = CarbonPeriod::create($start, $end);
// No need to send network request - just generate 1s // No need to send network request - just generate 1s
if ($currency === config('investbrain.base_currency')) { if ($currency === config('investbrain.base_currency')) {
dump('same curr');
$dateRange = []; $dateRange = [];
foreach ($period as $date) { foreach ($period as $date) {
$dateRange[$date->toDateString()] = 1; $dateRange[$date->toDateString()] = 1;
} }
return $dateRange; return $dateRange;
} }
dump('diff curr');
[$currency, $adjustment] = self::getCurrencyAliasAdjustments($currency); [$currency, $adjustment] = self::getCurrencyAliasAdjustments($currency);
$currencies = Currency::all()->pluck('currency')->toArray(); $currencies = Currency::all()->pluck('currency')->toArray();
dump('got currencies');
// call api in chunks // call api in chunks
$rates = [];
foreach (collect($period)->chunk(500) as $chunk) { foreach (collect($period)->chunk(500) as $chunk) {
dump('calling frankf time series');
$chunkRates = Frankfurter::setSymbols($currencies)->timeSeries($chunk->min(), $chunk->max()); $chunkRates = Frankfurter::setSymbols($currencies)->timeSeries($chunk->min(), $chunk->max());
$rates = array_merge($rates, Arr::get($chunkRates, 'rates', [])); $rates = Arr::get($chunkRates, 'rates', []);
}
// loop through each date // loop through each date
$updates = []; $updates = [];
foreach ($period as $date) {
$lookupDate = self::getNearestPastDate($date, $rates); foreach ($chunk as $date) {
if (is_null($lookupDate)) { $lookupDate = self::getNearestPastDate($date, $rates);
continue;
}
// loop through each rate if (is_null($lookupDate)) {
foreach ($rates[$lookupDate->toDateString()] as $curr => $rate) { continue;
}
// loop through each rate
foreach ($rates[$lookupDate->toDateString()] as $curr => $rate) {
// add to updates
$updates[] = [
'currency' => $curr,
'date' => $date->toDateString(),
'rate' => $rate,
'updated_at' => now()->toDateTimeString(),
'created_at' => now()->toDateTimeString(),
];
}
dump('inserting');
// persist
self::chunkInsert($updates);
// add to updates
$updates[] = [
'currency' => $curr,
'date' => $date->toDateString(),
'rate' => $rate,
'updated_at' => now()->toDateTimeString(),
'created_at' => now()->toDateTimeString(),
];
} }
} }
// persist dump('done');
self::chunkInsert($updates);
return collect($updates) return collect($updates)
->whereBetween('date', [$start, $end ?? now()]) ->whereBetween('date', [$start, $end ?? now()])
@@ -183,6 +199,7 @@ class CurrencyRate extends Model
private static function getNearestPastDate(CarbonInterface $date, array $rates): ?CarbonInterface private static function getNearestPastDate(CarbonInterface $date, array $rates): ?CarbonInterface
{ {
$datesWithRates = array_keys($rates); $datesWithRates = array_keys($rates);
sort($datesWithRates); sort($datesWithRates);
@@ -248,7 +265,7 @@ class CurrencyRate extends Model
public static function chunkInsert(array $updates): void public static function chunkInsert(array $updates): void
{ {
$chunks = array_chunk($updates, 250); $chunks = array_chunk($updates, 500);
foreach ($chunks as $chunk) { foreach ($chunks as $chunk) {
+25 -5
View File
@@ -95,19 +95,36 @@ class Dividend extends Model
return; return;
} }
// get some data dump('1. getting div data for '.$symbol);
if ($dividend_data = collect() && $start_date && $end_date) { try {
$dividend_data = app(MarketDataInterface::class)->dividends($symbol, $start_date, $end_date);
// get some data
if ($dividend_data = collect() && $start_date && $end_date) {
$dividend_data = app(MarketDataInterface::class)->dividends($symbol, $start_date, $end_date);
}
} catch (\Throwable $e) {
dump('exception: '.$e->getMessage());
} }
dump('2. got div data for '.$symbol);
// ah, we found some dividends... // ah, we found some dividends...
if ($dividend_data->isNotEmpty()) { if ($dividend_data->isNotEmpty()) {
$dividend_data = $dividend_data->sortBy('date');
dump('3. getting mkt data for '.$symbol);
$market_data = MarketData::getMarketData($symbol); $market_data = MarketData::getMarketData($symbol);
// get historic conversion rates dump('4. got market data for '.$symbol);
$rate_to_base = CurrencyRate::timeSeriesRates($market_data->currency, $start_date, $end_date);
// todo: use this for start_date - $dividend_data->first()->get('date')
// get historic conversion rates
$rate_to_base = CurrencyRate::timeSeriesRates($market_data->currency, $dividend_data->first()->get('date'), $end_date);
dump('5. got time series for '.$symbol);
// create mass insert // create mass insert
foreach ($dividend_data as $index => $dividend) { foreach ($dividend_data as $index => $dividend) {
$rate_to_base_date = 1 / Arr::get($rate_to_base, Carbon::parse(Arr::get($dividend, 'date'))->toDateString(), 1); $rate_to_base_date = 1 / Arr::get($rate_to_base, Carbon::parse(Arr::get($dividend, 'date'))->toDateString(), 1);
@@ -120,6 +137,7 @@ class Dividend extends Model
// insert records // insert records
(new self)->insertOrIgnore($dividend_data->toArray()); (new self)->insertOrIgnore($dividend_data->toArray());
dump('6. inserted for '.$symbol);
// sync to holdings // sync to holdings
self::syncHoldings($symbol); self::syncHoldings($symbol);
@@ -129,7 +147,9 @@ class Dividend extends Model
// sync last dividend amount to market data table // sync last dividend amount to market data table
$market_data->last_dividend_amount = $dividend_data->sortByDesc('date')->first()['dividend_amount']; $market_data->last_dividend_amount = $dividend_data->sortByDesc('date')->first()['dividend_amount'];
$market_data->save(); $market_data->save();
} }
} }
public static function syncHoldings(string $symbol): void public static function syncHoldings(string $symbol): void
+1 -1
View File
@@ -104,7 +104,7 @@ class User extends Authenticatable implements MustVerifyEmail
$options = is_array($key) ? $key : [$key => $value]; $options = is_array($key) ? $key : [$key => $value];
$this->user->options = array_merge($this->user->options ?? [], $options); $this->options = array_merge($this->options ?? [], $options);
return $this; return $this;
} }