diff --git a/app/Interfaces/MarketData/YahooMarketData.php b/app/Interfaces/MarketData/YahooMarketData.php index 737ffea..3fe88db 100644 --- a/app/Interfaces/MarketData/YahooMarketData.php +++ b/app/Interfaces/MarketData/YahooMarketData.php @@ -2,27 +2,28 @@ namespace App\Interfaces\MarketData; -use App\Models\Holding; use Illuminate\Support\Collection; use Scheb\YahooFinanceApi\ApiClientFactory; class YahooMarketData implements MarketDataInterface { - // api client public $client; public function __construct() { + // create yahoo finance client factory $this->client = ApiClientFactory::createApiClient(); } public function exists(String $symbol): Bool { + return $this->quote($symbol)->isNotEmpty(); } public function quote($symbol): Collection { + $quote = $this->client->getQuote($symbol); if (empty($quote)) return collect(); @@ -33,13 +34,18 @@ class YahooMarketData implements MarketDataInterface 'market_value' => $quote->getRegularMarketPrice(), 'fifty_two_week_high' => $quote->getFiftyTwoWeekHigh(), 'fifty_two_week_low' => $quote->getFiftyTwoWeekLow(), + 'forward_pe' => $quote->getForwardPE(), + 'trailing_pe' => $quote->getTrailingPE(), + 'market_cap' => $quote->getMarketCap() ]); } public function dividends($symbol, $startDate, $endDate): Collection { + return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate)) ->map(function($dividend) use ($symbol) { + return [ 'symbol' => $symbol, 'date' => $dividend->getDate()->format('Y-m-d H:i:s'), @@ -50,6 +56,7 @@ class YahooMarketData implements MarketDataInterface public function splits($symbol, $startDate, $endDate): Collection { + return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate)) ->map(function($split) use ($symbol) { $split_amount = explode(':', $split->getStockSplits()); diff --git a/app/Models/Holding.php b/app/Models/Holding.php index bc7b3a0..605642b 100644 --- a/app/Models/Holding.php +++ b/app/Models/Holding.php @@ -14,11 +14,6 @@ class Holding extends Model protected $with = ['market_data']; - /** - * The attributes that are mass assignable. - * - * @var array - */ protected $fillable = [ 'portfolio_id', 'symbol', @@ -31,16 +26,29 @@ class Holding extends Model 'dividends_synced_at' ]; - /** - * The attributes that should be cast to native types. - * - * @var array - */ protected $casts = [ 'splits_synced_at' => 'datetime', 'dividends_synced_at' => 'datetime', ]; + protected $appends = [ + 'market_gain_percent' + ]; + + /** + * Append the market gain / loss percent attribute + * + * @return int + * + */ + public function getMarketGainPercentAttribute() + { + + return (int) !empty($this->market_data?->market_value) && !empty($this->average_cost_basis) + ? (($this->market_data->market_value - $this->average_cost_basis) / $this->average_cost_basis) * 100 + : 0; + } + /** * Market data for holding * diff --git a/app/Models/MarketData.php b/app/Models/MarketData.php index cada5f0..dcd7dc5 100644 --- a/app/Models/MarketData.php +++ b/app/Models/MarketData.php @@ -25,6 +25,9 @@ class MarketData extends Model 'market_value', 'fifty_two_week_high', 'fifty_two_week_low', + 'forward_pe', + 'trailing_pe', + 'market_cap' ]; public static function setSplitsHoldingSynced($symbol) @@ -49,15 +52,17 @@ class MarketData extends Model ]); // check if new or stale - if (!$market_data->exists || now()->diffInMinutes($market_data->updated_at) >= config('market_data.refresh')) { + if ( + !$market_data->exists + || is_null($market_data->updated_at) + || $market_data->updated_at->diffInMinutes(now()) >= config('market_data.refresh') + ) { // get quote - // $quote = app(MarketDataInterface::class)->quote($symbol); + $quote = app(MarketDataInterface::class)->quote($symbol); // fill data - // $market_data->fill($quote->toArray()); - - + $market_data->fill($quote->toArray()); } // save with timestamps updated diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 70a05dd..0295981 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Models\MarketData; +use Illuminate\Support\Arr; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUuids; @@ -143,6 +144,17 @@ class Transaction extends Model * @return void */ public function syncHolding() { + + // sync previous symbol too + if (Arr::has($this->changes, 'symbol')) { + + $temp = new Transaction; + $temp->symbol = $this->original['symbol']; + $temp->portfolio_id = $this->portfolio_id; + + $temp->syncHolding(); + } + // get the holding for a symbol and portfolio (or create one) $holding = Holding::firstOrNew([ 'portfolio_id' => $this->portfolio_id, diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..e9971df 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,7 +11,16 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + + $market_data = config( + "market_data." . + config('market_data.default', 'yahoo') + ); + + $this->app->bind( + \App\Interfaces\MarketData\MarketDataInterface::class, + $market_data + ); } /** diff --git a/app/Rules/SymbolValidationRule.php b/app/Rules/SymbolValidationRule.php new file mode 100644 index 0000000..428aefd --- /dev/null +++ b/app/Rules/SymbolValidationRule.php @@ -0,0 +1,45 @@ +symbol = $value; + + if (MarketData::find($this->symbol)) { + + return; + } + + // Check if the symbol exists in the Market Data table first (avoid API call) + if (!app(MarketDataInterface::class)->exists($value)) { + $fail('The symbol provided (' . $this->symbol . ') is not valid'); + } + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index c490d9d..4f18154 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "maatwebsite/excel": "^3.1", "predis/predis": "^2.2", "robsontenorio/mary": "^1.35", + "scheb/yahoo-finance-api": "^4.10", "staudenmeir/eloquent-has-many-deep": "^1.20" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 6a2bb40..daf2143 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": "87ff0113a9121d64fb4518b040767e96", + "content-hash": "3d6ea585015c0a9442f1faf0d5c9b141", "packages": [ { "name": "bacon/bacon-qr-code", @@ -131,16 +131,16 @@ }, { "name": "blade-ui-kit/blade-icons", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/blade-ui-kit/blade-icons.git", - "reference": "74275f44c71e815b85bf7cea66e3bf98c57fb7e4" + "reference": "8f787baf09d88cdfd6ec4dbaba11ebfa885f0595" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/blade-ui-kit/blade-icons/zipball/74275f44c71e815b85bf7cea66e3bf98c57fb7e4", - "reference": "74275f44c71e815b85bf7cea66e3bf98c57fb7e4", + "url": "https://api.github.com/repos/blade-ui-kit/blade-icons/zipball/8f787baf09d88cdfd6ec4dbaba11ebfa885f0595", + "reference": "8f787baf09d88cdfd6ec4dbaba11ebfa885f0595", "shasum": "" }, "require": { @@ -208,7 +208,7 @@ "type": "paypal" } ], - "time": "2024-07-29T21:49:30+00:00" + "time": "2024-08-14T14:25:11+00:00" }, { "name": "brick/math", @@ -422,23 +422,23 @@ }, { "name": "dasprid/enum", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", "shasum": "" }, "require": { "php": ">=7.1 <9.0" }, "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", "squizlabs/php_codesniffer": "*" }, "type": "library", @@ -466,9 +466,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" }, - "time": "2023-08-25T16:18:39+00:00" + "time": "2024-08-09T14:30:48+00:00" }, { "name": "dflydev/dot-access-data", @@ -1685,16 +1685,16 @@ }, { "name": "laravel/fortify", - "version": "v1.22.0", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "33f8af0d4d11c4d30c47b450d097815d0eebd665" + "reference": "fbe67f018c1fe26d00913de56a6d60589b4be9b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/33f8af0d4d11c4d30c47b450d097815d0eebd665", - "reference": "33f8af0d4d11c4d30c47b450d097815d0eebd665", + "url": "https://api.github.com/repos/laravel/fortify/zipball/fbe67f018c1fe26d00913de56a6d60589b4be9b2", + "reference": "fbe67f018c1fe26d00913de56a6d60589b4be9b2", "shasum": "" }, "require": { @@ -1746,20 +1746,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-07-22T14:37:15+00:00" + "time": "2024-08-20T14:43:56+00:00" }, { "name": "laravel/framework", - "version": "v11.18.1", + "version": "v11.21.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "b19ba518c56852567e99fbae9321bc436c2cc5a8" + "reference": "9d9d36708d56665b12185493f684abce38ad2d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/b19ba518c56852567e99fbae9321bc436c2cc5a8", - "reference": "b19ba518c56852567e99fbae9321bc436c2cc5a8", + "url": "https://api.github.com/repos/laravel/framework/zipball/9d9d36708d56665b12185493f684abce38ad2d30", + "reference": "9d9d36708d56665b12185493f684abce38ad2d30", "shasum": "" }, "require": { @@ -1952,20 +1952,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-07-26T10:39:29+00:00" + "time": "2024-08-20T15:00:52+00:00" }, { "name": "laravel/jetstream", - "version": "v5.1.4", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/laravel/jetstream.git", - "reference": "0eecfe8554e934d15c73cba5fd6c7f30ed640f3d" + "reference": "653a422fe65278c1c4f319e99d5cb700c4117ea0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/jetstream/zipball/0eecfe8554e934d15c73cba5fd6c7f30ed640f3d", - "reference": "0eecfe8554e934d15c73cba5fd6c7f30ed640f3d", + "url": "https://api.github.com/repos/laravel/jetstream/zipball/653a422fe65278c1c4f319e99d5cb700c4117ea0", + "reference": "653a422fe65278c1c4f319e99d5cb700c4117ea0", "shasum": "" }, "require": { @@ -2019,20 +2019,20 @@ "issues": "https://github.com/laravel/jetstream/issues", "source": "https://github.com/laravel/jetstream" }, - "time": "2024-07-10T19:01:59+00:00" + "time": "2024-08-08T13:28:23+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.24", + "version": "v0.1.25", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "409b0b4305273472f3754826e68f4edbd0150149" + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/409b0b4305273472f3754826e68f4edbd0150149", - "reference": "409b0b4305273472f3754826e68f4edbd0150149", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", "shasum": "" }, "require": { @@ -2075,9 +2075,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.24" + "source": "https://github.com/laravel/prompts/tree/v0.1.25" }, - "time": "2024-06-17T13:58:22+00:00" + "time": "2024-08-12T22:06:33+00:00" }, { "name": "laravel/sanctum", @@ -2145,26 +2145,27 @@ }, { "name": "laravel/serializable-closure", - "version": "v1.3.3", + "version": "v1.3.4", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", - "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/61b87392d986dc49ad5ef64e75b1ff5fee24ef81", + "reference": "61b87392d986dc49ad5ef64e75b1ff5fee24ef81", "shasum": "" }, "require": { "php": "^7.3|^8.0" }, "require-dev": { - "nesbot/carbon": "^2.61", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", "pestphp/pest": "^1.21.3", "phpstan/phpstan": "^1.8.2", - "symfony/var-dumper": "^5.4.11" + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" }, "type": "library", "extra": { @@ -2201,7 +2202,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2023-11-08T14:08:06+00:00" + "time": "2024-08-02T07:48:17+00:00" }, { "name": "laravel/tinker", @@ -2271,16 +2272,16 @@ }, { "name": "league/commonmark", - "version": "2.5.1", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c" + "reference": "b650144166dfa7703e62a22e493b853b58d874b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/ac815920de0eff6de947eac0a6a94e5ed0fb147c", - "reference": "ac815920de0eff6de947eac0a6a94e5ed0fb147c", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", + "reference": "b650144166dfa7703e62a22e493b853b58d874b0", "shasum": "" }, "require": { @@ -2293,8 +2294,8 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.31.0", - "commonmark/commonmark.js": "0.31.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", @@ -2373,7 +2374,7 @@ "type": "tidelift" } ], - "time": "2024-07-24T12:52:09+00:00" + "time": "2024-08-16T11:46:16+00:00" }, { "name": "league/config", @@ -2647,16 +2648,16 @@ }, { "name": "livewire/livewire", - "version": "v3.5.4", + "version": "v3.5.6", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "b158c6386a892efc6c5e4682e682829baac1f933" + "reference": "597a2808d8d3001cc3ed5ce89a6ebab00f83b80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/b158c6386a892efc6c5e4682e682829baac1f933", - "reference": "b158c6386a892efc6c5e4682e682829baac1f933", + "url": "https://api.github.com/repos/livewire/livewire/zipball/597a2808d8d3001cc3ed5ce89a6ebab00f83b80f", + "reference": "597a2808d8d3001cc3ed5ce89a6ebab00f83b80f", "shasum": "" }, "require": { @@ -2664,6 +2665,7 @@ "illuminate/routing": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0", "illuminate/validation": "^10.0|^11.0", + "laravel/prompts": "^0.1.24", "league/mime-type-detection": "^1.9", "php": "^8.1", "symfony/console": "^6.0|^7.0", @@ -2672,7 +2674,6 @@ "require-dev": { "calebporzio/sushi": "^2.1", "laravel/framework": "^10.15.0|^11.0", - "laravel/prompts": "^0.1.6", "mockery/mockery": "^1.3.1", "orchestra/testbench": "^8.21.0|^9.0", "orchestra/testbench-dusk": "^8.24|^9.1", @@ -2711,7 +2712,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.5.4" + "source": "https://github.com/livewire/livewire/tree/v3.5.6" }, "funding": [ { @@ -2719,7 +2720,7 @@ "type": "github" } ], - "time": "2024-07-15T18:27:32+00:00" + "time": "2024-08-19T11:52:18+00:00" }, { "name": "livewire/volt", @@ -2795,16 +2796,16 @@ }, { "name": "maatwebsite/excel", - "version": "3.1.55", + "version": "3.1.56", "source": { "type": "git", "url": "https://github.com/SpartnerNL/Laravel-Excel.git", - "reference": "6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260" + "reference": "0381d0225b42c3f328d90f0dd05ca071fca3953f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260", - "reference": "6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/0381d0225b42c3f328d90f0dd05ca071fca3953f", + "reference": "0381d0225b42c3f328d90f0dd05ca071fca3953f", "shasum": "" }, "require": { @@ -2860,7 +2861,7 @@ ], "support": { "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", - "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.55" + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.56" }, "funding": [ { @@ -2872,7 +2873,7 @@ "type": "github" } ], - "time": "2024-02-20T08:27:10+00:00" + "time": "2024-08-19T09:40:43+00:00" }, { "name": "maennchen/zipstream-php", @@ -3229,16 +3230,16 @@ }, { "name": "nesbot/carbon", - "version": "3.7.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139" + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cb4374784c87d0a0294e8513a52eb63c0aff3139", - "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f", + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f", "shasum": "" }, "require": { @@ -3331,7 +3332,7 @@ "type": "tidelift" } ], - "time": "2024-07-16T22:29:20+00:00" + "time": "2024-08-19T06:22:39+00:00" }, { "name": "nette/schema", @@ -3397,20 +3398,20 @@ }, { "name": "nette/utils", - "version": "v4.0.4", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", "shasum": "" }, "require": { - "php": ">=8.0 <8.4" + "php": "8.0 - 8.4" }, "conflict": { "nette/finder": "<3", @@ -3477,9 +3478,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.4" + "source": "https://github.com/nette/utils/tree/v4.0.5" }, - "time": "2024-01-17T16:50:36+00:00" + "time": "2024-08-07T15:39:19+00:00" }, { "name": "nikic/php-parser", @@ -4300,16 +4301,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { @@ -4344,9 +4345,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.1" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { "name": "psr/simple-cache", @@ -4705,16 +4706,16 @@ }, { "name": "robsontenorio/mary", - "version": "1.35.3", + "version": "1.35.6", "source": { "type": "git", "url": "https://github.com/robsontenorio/mary.git", - "reference": "5b641d64b8cd2f8602758eb0e2c6ad0ae68cb6a9" + "reference": "46294f8e692836109011b0264e006adb49bf2506" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robsontenorio/mary/zipball/5b641d64b8cd2f8602758eb0e2c6ad0ae68cb6a9", - "reference": "5b641d64b8cd2f8602758eb0e2c6ad0ae68cb6a9", + "url": "https://api.github.com/repos/robsontenorio/mary/zipball/46294f8e692836109011b0264e006adb49bf2506", + "reference": "46294f8e692836109011b0264e006adb49bf2506", "shasum": "" }, "require": { @@ -4780,7 +4781,7 @@ ], "support": { "issues": "https://github.com/robsontenorio/mary/issues", - "source": "https://github.com/robsontenorio/mary/tree/1.35.3" + "source": "https://github.com/robsontenorio/mary/tree/1.35.6" }, "funding": [ { @@ -4788,7 +4789,62 @@ "type": "github" } ], - "time": "2024-07-26T15:41:31+00:00" + "time": "2024-08-17T21:24:32+00:00" + }, + { + "name": "scheb/yahoo-finance-api", + "version": "v4.10.1", + "source": { + "type": "git", + "url": "https://github.com/scheb/yahoo-finance-api.git", + "reference": "9a43debe17ce90a9aeea9c998aa1ecce2bd9f8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/yahoo-finance-api/zipball/9a43debe17ce90a9aeea9c998aa1ecce2bd9f8ed", + "reference": "9a43debe17ce90a9aeea9c998aa1ecce2bd9f8ed", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "php": ">=7.1.3" + }, + "require-dev": { + "escapestudios/symfony2-coding-standard": "^3.9", + "phpunit/phpunit": "^7.5 || ^8 || ^9", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^3.11|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Scheb\\YahooFinanceApi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "PHP library for accessing Yahoo Finance data", + "homepage": "https://github.com/scheb/yahoo-finance-api", + "keywords": [ + "api", + "finance", + "stock", + "yahoo" + ], + "support": { + "issues": "https://github.com/scheb/yahoo-finance-api/issues", + "source": "https://github.com/scheb/yahoo-finance-api/tree/v4.10.1" + }, + "time": "2024-06-09T11:50:56+00:00" }, { "name": "staudenmeir/eloquent-has-many-deep", @@ -7656,16 +7712,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.0", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", - "reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -7676,13 +7732,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -7718,20 +7774,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-07-23T16:40:20+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "laravel/sail", - "version": "v1.31.0", + "version": "v1.31.1", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "48d89608a3bb5be763c9bb87121d31e7da27c1cb" + "reference": "3d06dd18cee8059baa7b388af00ba47f6d96bd85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/48d89608a3bb5be763c9bb87121d31e7da27c1cb", - "reference": "48d89608a3bb5be763c9bb87121d31e7da27c1cb", + "url": "https://api.github.com/repos/laravel/sail/zipball/3d06dd18cee8059baa7b388af00ba47f6d96bd85", + "reference": "3d06dd18cee8059baa7b388af00ba47f6d96bd85", "shasum": "" }, "require": { @@ -7781,7 +7837,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2024-07-22T14:36:50+00:00" + "time": "2024-08-02T07:45:47+00:00" }, { "name": "mockery/mockery", @@ -7928,23 +7984,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.3.0", + "version": "v8.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "b49f5b2891ce52726adfd162841c69d4e4c84229" + "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/b49f5b2891ce52726adfd162841c69d4e4c84229", - "reference": "b49f5b2891ce52726adfd162841c69d4e4c84229", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/e7d1aa8ed753f63fa816932bbc89678238843b4a", + "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a", "shasum": "" }, "require": { "filp/whoops": "^2.15.4", "nunomaduro/termwind": "^2.0.1", "php": "^8.2.0", - "symfony/console": "^7.1.2" + "symfony/console": "^7.1.3" }, "conflict": { "laravel/framework": "<11.0.0 || >=12.0.0", @@ -7952,13 +8008,13 @@ }, "require-dev": { "larastan/larastan": "^2.9.8", - "laravel/framework": "^11.16.0", - "laravel/pint": "^1.16.2", - "laravel/sail": "^1.30.2", + "laravel/framework": "^11.19.0", + "laravel/pint": "^1.17.1", + "laravel/sail": "^1.31.0", "laravel/sanctum": "^4.0.2", "laravel/tinker": "^2.9.0", - "orchestra/testbench-core": "^9.2.1", - "pestphp/pest": "^2.34.9 || ^3.0.0", + "orchestra/testbench-core": "^9.2.3", + "pestphp/pest": "^2.35.0 || ^3.0.0", "sebastian/environment": "^6.1.0 || ^7.0.0" }, "type": "library", @@ -8021,7 +8077,7 @@ "type": "patreon" } ], - "time": "2024-07-16T22:41:01+00:00" + "time": "2024-08-03T15:32:23+00:00" }, { "name": "phar-io/manifest", @@ -8143,32 +8199,32 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.5", + "version": "11.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45", + "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.1.0", "php": ">=8.2", - "phpunit/php-file-iterator": "^5.0", - "phpunit/php-text-template": "^4.0", - "sebastian/code-unit-reverse-lookup": "^4.0", - "sebastian/complexity": "^4.0", - "sebastian/environment": "^7.0", - "sebastian/lines-of-code": "^3.0", - "sebastian/version": "^5.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^11.0" @@ -8180,7 +8236,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -8209,7 +8265,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6" }, "funding": [ { @@ -8217,7 +8273,7 @@ "type": "github" } ], - "time": "2024-07-03T05:05:37+00:00" + "time": "2024-08-22T04:37:56+00:00" }, { "name": "phpunit/php-file-iterator", @@ -8466,16 +8522,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.2.8", + "version": "11.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39" + "reference": "fe179875ef0c14e90b75617002767eae0a742641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7a29e8d3113806f18f99d670f580a30e8ffff39", - "reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fe179875ef0c14e90b75617002767eae0a742641", + "reference": "fe179875ef0c14e90b75617002767eae0a742641", "shasum": "" }, "require": { @@ -8496,7 +8552,7 @@ "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.0.1", + "sebastian/comparator": "^6.0.2", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.1.3", @@ -8514,7 +8570,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.2-dev" + "dev-main": "11.3-dev" } }, "autoload": { @@ -8546,7 +8602,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.1" }, "funding": [ { @@ -8562,7 +8618,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T14:56:37+00:00" + "time": "2024-08-13T06:14:23+00:00" }, { "name": "sebastian/cli-parser", @@ -8736,16 +8792,16 @@ }, { "name": "sebastian/comparator", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "131942b86d3587291067a94f295498ab6ac79c20" + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/131942b86d3587291067a94f295498ab6ac79c20", - "reference": "131942b86d3587291067a94f295498ab6ac79c20", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/450d8f237bd611c45b5acf0733ce43e6bb280f81", + "reference": "450d8f237bd611c45b5acf0733ce43e6bb280f81", "shasum": "" }, "require": { @@ -8801,7 +8857,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.2" }, "funding": [ { @@ -8809,7 +8865,7 @@ "type": "github" } ], - "time": "2024-07-03T04:48:07+00:00" + "time": "2024-08-12T06:07:25+00:00" }, { "name": "sebastian/complexity", diff --git a/config/market_data.php b/config/market_data.php new file mode 100644 index 0000000..528ccb5 --- /dev/null +++ b/config/market_data.php @@ -0,0 +1,11 @@ + 30, // minutes + + 'default' => env('MARKET_DATA_PROVIDER', 'yahoo'), + + 'yahoo' => App\Interfaces\MarketData\YahooMarketData::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 7e493c7..12ca625 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,6 +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->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->json('meta_data')->nullable(); $table->timestamps(); }); } diff --git a/database/seeders/MarketDataSeeder.php b/database/seeders/MarketDataSeeder.php new file mode 100644 index 0000000..2b5a443 --- /dev/null +++ b/database/seeders/MarketDataSeeder.php @@ -0,0 +1,81 @@ + $data['symbol'], + 'name' => $data['name'], + 'meta_data' => json_encode([ + 'country' => $data['country'], + 'first_trade_year' => $data['first_trade_year'], + 'sector' => $data['sector'], + 'industry' => $data['industry'], + ]), + ]; + + $rowCount++; + + if ($rowCount % $chunkSize == 0) { + DB::table('market_data')->insertOrIgnore($rows); + $rows = []; + } + } catch (\Throwable $e) { + + throw new \Exception('Error: '. $e->getMessage()); + } + } + } + + // final clean up + if (!empty($rows)) { + DB::table('market_data')->insertOrIgnore($rows); + } + + // Close the CSV file + fclose($handle); + + echo "Imported $rowCount items successfully!\n"; + + } else { + + echo "Failed to open the CSV.\n"; + } + } +} diff --git a/resources/views/livewire/manage-transaction-form.blade.php b/resources/views/livewire/manage-transaction-form.blade.php index cd82566..129f2ff 100644 --- a/resources/views/livewire/manage-transaction-form.blade.php +++ b/resources/views/livewire/manage-transaction-form.blade.php @@ -2,11 +2,11 @@ use App\Models\Transaction; use App\Models\Portfolio; +use App\Rules\SymbolValidationRule; use Illuminate\Support\Collection; -use Livewire\Attributes\Rule; +use Livewire\Attributes\{Computed}; use Livewire\Volt\Component; use Mary\Traits\Toast; -use Livewire\Attributes\Computed; new class extends Component { use Toast; @@ -15,27 +15,29 @@ new class extends Component { public ?Portfolio $portfolio; public ?Transaction $transaction; - #[Rule('required|string|max:15')] public String $symbol; - - #[Rule('required|string|in:BUY,SELL')] public String $transaction_type; - - #[Rule('required|date_format:Y-m-d')] public String $date; - - #[Rule('required|min:0|numeric')] public Float $quantity; - - #[Rule('exclude_if:transaction_type,SELL|min:0|numeric')] public ?Float $cost_basis; - - #[Rule('exclude_if:transaction_type,BUY|min:0|numeric')] public ?Float $sale_price; public Bool $confirmingTransactionDeletion = false; // methods + public function rules() + { + + return [ + 'symbol' => ['required', 'string', new SymbolValidationRule], + 'transaction_type' => 'required|string|in:BUY,SELL', + 'date' => 'required|date_format:Y-m-d', + 'quantity' => 'required|min:0|numeric', + 'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric', + 'sale_price' => 'exclude_if:transaction_type,BUY|min:0|numeric', + ]; + } + public function mount() { if (isset($this->transaction)) { @@ -128,7 +130,13 @@ new class extends Component { /> @endif - + diff --git a/resources/views/livewire/top-performers-list.blade.php b/resources/views/livewire/top-performers-list.blade.php index 6b7b733..908e589 100644 --- a/resources/views/livewire/top-performers-list.blade.php +++ b/resources/views/livewire/top-performers-list.blade.php @@ -17,7 +17,7 @@ new class extends Component {
@foreach( - $holdings->sortBy('market_gain_percent') + $holdings->sortByDesc('market_gain_percent') ->where('quantity', '>', 0) ->where('market_data.market_value', '>', 0) ->take(5) @@ -29,17 +29,13 @@ new class extends Component { link="{{ route('portfolio.show', ['portfolio' => $holding->portfolio_id]) }}" > - @php - $gainPercent = (($holding->market_data->market_value - $holding->average_cost_basis) / $holding->average_cost_basis) * 100; - @endphp - {{ $holding->market_data?->name }} ({{ $holding->symbol }}) - + - {{ Number::percentage($gainPercent) }} + {{ Number::percentage($holding->market_gain_percent) }}