Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c691ee922a | |||
| 3eb9bad840 | |||
| 370f7bb54b | |||
| 62bf6797e6 | |||
| c4e3645145 | |||
| 69e4d0fb3a | |||
| 20c2cb37cc | |||
| d2bb065822 | |||
| 0c00f28d97 | |||
| 5eab00ee33 | |||
| 56064ad84e | |||
| c96ff0e45f | |||
| 33e0df5ae2 | |||
| a5a333f784 | |||
| 89b5505e1d | |||
| 60923b3c93 | |||
| 17e5d8b665 | |||
| bd9c828c68 | |||
| f72cd6f5a7 | |||
| 3593697cce | |||
| d53e71dcd5 | |||
| 71e79cfb40 |
@@ -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). |
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -50,4 +50,9 @@ class BackupImport extends Model
|
|||||||
'completed_at' => 'datetime',
|
'completed_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-23
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user