diff --git a/.env.example b/.env.example index 85f0a2d..47868df 100644 --- a/.env.example +++ b/.env.example @@ -56,6 +56,8 @@ MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +MARKET_DATA_PROVIDER=yahoo +MARKET_DATA_REFRESH=30 ALPHAVANTAGE_API_KEY= AWS_ACCESS_KEY_ID= diff --git a/app/Interfaces/MarketData/AlphaVantageMarketData.php b/app/Interfaces/MarketData/AlphaVantageMarketData.php index 6e223d7..30ad653 100644 --- a/app/Interfaces/MarketData/AlphaVantageMarketData.php +++ b/app/Interfaces/MarketData/AlphaVantageMarketData.php @@ -17,26 +17,40 @@ class AlphaVantageMarketData implements MarketDataInterface public function quote($symbol): Collection { - $fundamental = Alphavantage::fundamentals()->overview($symbol); $quote = Alphavantage::core()->quoteEndpoint($symbol); + $quote = Arr::get($quote, 'Global Quote', []); + + $fundamental = cache()->tags(['quote', 'alpha-vantage', $symbol])->remember( + 'symbol-'.$symbol, + 1440, + function () use ($symbol) { + return Alphavantage::fundamentals()->overview($symbol); + } + ); if (empty($fundamental)) return collect(); return collect([ 'name' => Arr::get($fundamental, 'Name'), 'symbol' => Arr::get($fundamental, 'Symbol'), - 'market_value' => Arr::get($quote, 'Global Quote')['05. price'], + 'market_value' => Arr::get($quote, '05. price'), 'fifty_two_week_high' => Arr::get($fundamental, '52WeekHigh'), 'fifty_two_week_low' => Arr::get($fundamental, '52WeekLow'), 'forward_pe' => Arr::get($fundamental, 'ForwardPE'), 'trailing_pe' => Arr::get($fundamental, 'TrailingPE'), - 'market_cap' => Arr::get($fundamental, 'MarketCapitalization') + 'market_cap' => Arr::get($fundamental, 'MarketCapitalization'), + 'book_value' => Arr::get($fundamental, 'BookValue'), + 'last_dividend_date' => Arr::get($fundamental, 'DividendDate') != 'None' + ? Arr::get($fundamental, 'DividendDate') + : null, + 'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None' + ? Arr::get($fundamental, 'DividendYield') + : null ]); } public function dividends($symbol, $startDate, $endDate): Collection { - $dividends = Alphavantage::fundamentals()->dividends($symbol); return collect($dividends) diff --git a/app/Interfaces/MarketData/FakeMarketData.php b/app/Interfaces/MarketData/FakeMarketData.php index a90f687..28fe05f 100644 --- a/app/Interfaces/MarketData/FakeMarketData.php +++ b/app/Interfaces/MarketData/FakeMarketData.php @@ -23,7 +23,10 @@ class FakeMarketData implements MarketDataInterface 'fifty_two_week_low' => 341.20, 'forward_pe' => 20.1, 'trailing_pe' => 30.34, - 'market_cap' => 9800700600 + 'market_cap' => 9800700600, + 'book_value' => 4.7, + 'last_dividend_date' => now()->subDays(45), + 'dividend_yield' => .033 ]); } diff --git a/app/Interfaces/MarketData/YahooMarketData.php b/app/Interfaces/MarketData/YahooMarketData.php index 340a3c9..8c5f9d1 100644 --- a/app/Interfaces/MarketData/YahooMarketData.php +++ b/app/Interfaces/MarketData/YahooMarketData.php @@ -36,7 +36,10 @@ class YahooMarketData implements MarketDataInterface 'fifty_two_week_low' => $quote->getFiftyTwoWeekLow(), 'forward_pe' => $quote->getForwardPE(), 'trailing_pe' => $quote->getTrailingPE(), - 'market_cap' => $quote->getMarketCap() + 'market_cap' => $quote->getMarketCap(), + 'book_value' => $quote->getBookValue(), + 'last_dividend_date' => $quote->getDividendDate(), + 'dividend_yield' => $quote->getTrailingAnnualDividendYield() ]); } diff --git a/app/Models/Dividend.php b/app/Models/Dividend.php index 8879765..76fc17a 100644 --- a/app/Models/Dividend.php +++ b/app/Models/Dividend.php @@ -26,6 +26,18 @@ class Dividend extends Model 'last_date' => 'datetime', ]; + public function marketData() { + return $this->belongsTo(MarketData::class, 'symbol', 'symbol'); + } + + public function holdings() { + return $this->hasMany(Holding::class, 'symbol', 'symbol'); + } + + public function transactions() { + return $this->hasMany(Transaction::class, 'symbol', 'symbol'); + } + /** * Grab new dividend data * @@ -48,7 +60,6 @@ class Dividend extends Model if ( $dividends_meta->total_dividends ) { $start_date = $dividends_meta->last_date->addHours(48); - $end_date = now(); } // get some data @@ -67,48 +78,43 @@ class Dividend extends Model (new self)->insert($dividend_data->toArray()); // sync to holdings - $dividends = self::where([ - 'dividends.symbol' => $dividend_data->last()->symbol, - ])->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') - ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') - ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') - ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') - ->join('transactions', 'transactions.symbol', 'dividends.symbol') - ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') - ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) - ->get(); + self::syncHoldings($dividend_data); - // iterate through holdings and update - Holding::where(['symbol' => $symbol]) - ->get() - ->each(function ($holding) use ($dividends) { - $holding->update([ - 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)->sum('dividends_received') - ]); - }); - - // sync most last dividend date in market data + // sync last dividend amount to market data table $market_data = MarketData::symbol($symbol)->first(); - $dividend_data_latest_date = $dividend_data->sortByDesc('date')->first()['date']; - - if ($market_data->dividend_date < $dividend_data_latest_date) { - $market_data->update(['dividend_date' => $dividend_data_latest_date]); // why is this set to latest date? - } + $market_data->last_dividend_amount = $dividend_data->sortByDesc('date')->first()['amount']; + $market_data->save(); } return $dividend_data; } - public function marketData() { - return $this->belongsTo(MarketData::class, 'symbol', 'symbol'); - } + public static function syncHoldings($dividend_data): void + { + $symbol = $dividend_data->last()->symbol; - public function holdings() { - return $this->hasMany(Holding::class, 'symbol', 'symbol'); - } + // group by holdings + $dividends = self::where([ + 'dividends.symbol' => $dividend_data->last()->symbol, + ]) + ->select(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->selectRaw('@purchased:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "BUY" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `purchased`') + ->selectRaw('@sold:=(SELECT coalesce(SUM(quantity),0) FROM transactions WHERE transactions.transaction_type = "SELL" AND transactions.symbol = dividends.symbol AND date(transactions.date) <= date(dividends.date) AND holdings.portfolio_id = transactions.portfolio_id ) AS `sold`') + ->selectRaw('@owned:=(@purchased - @sold) AS `owned`') + ->selectRaw('@dividends_received:=(@owned * dividends.dividend_amount) AS `dividends_received`') + ->join('transactions', 'transactions.symbol', 'dividends.symbol') + ->join('holdings', 'transactions.portfolio_id', 'holdings.portfolio_id') + ->groupBy(['holdings.portfolio_id', 'dividends.date', 'dividends.symbol', 'dividends.dividend_amount']) + ->get(); - public function transactions() { - return $this->hasMany(Transaction::class, 'symbol', 'symbol'); + // iterate through holdings and update + Holding::where(['symbol' => $symbol]) + ->get() + ->each(function ($holding) use ($dividends) { + $holding->update([ + 'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id) + ->sum('dividends_received') + ]); + }); } } diff --git a/app/Models/MarketData.php b/app/Models/MarketData.php index 3caac5c..a7ff06a 100644 --- a/app/Models/MarketData.php +++ b/app/Models/MarketData.php @@ -22,16 +22,10 @@ class MarketData extends Model 'fifty_two_week_low', 'forward_pe', 'trailing_pe', - 'market_cap' - ]; - - protected $attributes = [ - 'market_value' => 0, - 'fifty_two_week_high' => 0, - 'fifty_two_week_low' => 0, - 'forward_pe' => 0, - 'trailing_pe' => 0, - 'market_cap' => 0 + 'market_cap', + 'book_value', + 'last_dividend_date', + 'dividend_yield' ]; public function holdings() diff --git a/composer.json b/composer.json index 83634a8..7b4b141 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "robsontenorio/mary": "^1.35", "scheb/yahoo-finance-api": "^4.10", "staudenmeir/eloquent-has-many-deep": "^1.20", - "tschucki/alphavantage-laravel": "^0.0.1" + "tschucki/alphavantage-laravel": "^0.0" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index be35f88..6347651 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "47988869f65dbf94a6228627c7806e1f", + "content-hash": "40816463478b2d00c6a7c18e71aa7e7c", "packages": [ { "name": "bacon/bacon-qr-code", @@ -7369,16 +7369,16 @@ }, { "name": "tschucki/alphavantage-laravel", - "version": "0.0.1", + "version": "0.0.2", "source": { "type": "git", "url": "https://github.com/Tschucki/alphavantage-laravel.git", - "reference": "409c78c123ed40a150d8ea6ac095ab36dc077f82" + "reference": "1317dab3b0b3b3866c974816ef31e745a888e596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Tschucki/alphavantage-laravel/zipball/409c78c123ed40a150d8ea6ac095ab36dc077f82", - "reference": "409c78c123ed40a150d8ea6ac095ab36dc077f82", + "url": "https://api.github.com/repos/Tschucki/alphavantage-laravel/zipball/1317dab3b0b3b3866c974816ef31e745a888e596", + "reference": "1317dab3b0b3b3866c974816ef31e745a888e596", "shasum": "" }, "require": { @@ -7436,7 +7436,7 @@ ], "support": { "issues": "https://github.com/Tschucki/alphavantage-laravel/issues", - "source": "https://github.com/Tschucki/alphavantage-laravel/tree/0.0.1" + "source": "https://github.com/Tschucki/alphavantage-laravel/tree/0.0.2" }, "funding": [ { @@ -7444,7 +7444,7 @@ "type": "github" } ], - "time": "2024-07-14T17:31:03+00:00" + "time": "2024-09-01T11:41:46+00:00" }, { "name": "vlucas/phpdotenv", diff --git a/config/market_data.php b/config/market_data.php index 528ccb5..63ea67c 100644 --- a/config/market_data.php +++ b/config/market_data.php @@ -2,10 +2,11 @@ return [ - 'refresh' => 30, // minutes + 'refresh' => env('MARKET_DATA_REFRESH', 30), // minutes 'default' => env('MARKET_DATA_PROVIDER', 'yahoo'), 'yahoo' => App\Interfaces\MarketData\YahooMarketData::class, - // 'fake' => App\Interfaces\MarketData\FakeMarketData::class, + 'alphavantage' => App\Interfaces\MarketData\AlphaVantageMarketData::class, + 'fake' => App\Interfaces\MarketData\FakeMarketData::class, ]; \ No newline at end of file diff --git a/database/migrations/2021_02_25_041221_create_market_data_table.php b/database/migrations/2021_02_25_041221_create_market_data_table.php index 12ca625..5676d84 100644 --- a/database/migrations/2021_02_25_041221_create_market_data_table.php +++ b/database/migrations/2021_02_25_041221_create_market_data_table.php @@ -21,10 +21,11 @@ class CreateMarketDataTable extends Migration $table->float('fifty_two_week_high', 12, 4)->nullable(); $table->timestamp('last_dividend_date')->nullable(); $table->float('last_dividend_amount', 12, 4)->nullable(); + $table->float('dividend_yield', 12, 4)->nullable(); $table->unsignedBigInteger('market_cap')->nullable(); $table->float('trailing_pe', 12, 4)->nullable(); $table->float('forward_pe', 12, 4)->nullable(); - $table->float('pe_growth', 12, 4)->nullable(); + $table->float('book_value', 12, 4)->nullable(); $table->json('meta_data')->nullable(); $table->timestamps(); }); diff --git a/routes/web.php b/routes/web.php index 671e99a..974bef2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,10 +1,14 @@ overview('TSLA'); + + $quote = Alphavantage::core()->quoteEndpoint('FFRHX'); + + $quote = Arr::get($quote, 'Global Quote', []); + + + return $quote; + + $client = ApiClientFactory::createApiClient(); + + return $client->getQuote("IBM"); + + return $client->getHistoricalQuoteData( + "AAPL", + ApiClient::INTERVAL_1_DAY, + new \DateTime("-14 days"), + new \DateTime("today") + ); + + return $client->getHistoricalDividendData( + "AAPL", + new \DateTime("-5 years"), + new \DateTime("today") + ); }); Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {