diff --git a/app/Console/Commands/RefreshMarketData.php b/app/Console/Commands/RefreshMarketData.php index 68325b1..5518581 100644 --- a/app/Console/Commands/RefreshMarketData.php +++ b/app/Console/Commands/RefreshMarketData.php @@ -7,6 +7,7 @@ use App\Models\Holding; use App\Models\MarketData; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; class RefreshMarketData extends Command { @@ -57,7 +58,11 @@ public function handle() foreach ($holdings->get() as $holding) { $this->line('Refreshing '.$holding->symbol); - MarketData::getMarketData($holding->symbol, $force); + try { + MarketData::getMarketData($holding->symbol, $force); + } catch (\Throwable $e) { + Log::error('Could not refresh '.$holding->symbol.' ('.$e->getMessage().')'); + } } } } diff --git a/app/Http/ApiControllers/MarketDataController.php b/app/Http/ApiControllers/MarketDataController.php index f128a68..a815ea6 100644 --- a/app/Http/ApiControllers/MarketDataController.php +++ b/app/Http/ApiControllers/MarketDataController.php @@ -14,8 +14,17 @@ class MarketDataController extends ApiController public function show(Request $request, string $symbol) { - return MarketDataResource::make( - MarketData::getMarketData($symbol) - ); + try { + + return MarketDataResource::make( + MarketData::getMarketData($symbol) + ); + } catch (\Throwable $e) { + + return response([ + 'message' => 'Symbol '.$symbol.' not found.', + ], 404); + } + } } diff --git a/app/Interfaces/MarketData/AlphaVantageMarketData.php b/app/Interfaces/MarketData/AlphaVantageMarketData.php index a139a60..17c4856 100644 --- a/app/Interfaces/MarketData/AlphaVantageMarketData.php +++ b/app/Interfaces/MarketData/AlphaVantageMarketData.php @@ -18,7 +18,7 @@ class AlphaVantageMarketData implements MarketDataInterface public function exists(string $symbol): bool { - return $this->quote($symbol)->isNotEmpty(); + return (bool) $this->quote($symbol); } public function quote(string $symbol): Quote @@ -26,10 +26,6 @@ public function quote(string $symbol): Quote $quote = Alphavantage::core()->quoteEndpoint($symbol); $quote = Arr::get($quote, 'Global Quote', []); - if (empty($quote)) { - return new Quote; - } - $fundamental = cache()->remember( 'av-symbol-'.$symbol, 1440, diff --git a/app/Interfaces/MarketData/FallbackInterface.php b/app/Interfaces/MarketData/FallbackInterface.php index 194758a..ff25749 100644 --- a/app/Interfaces/MarketData/FallbackInterface.php +++ b/app/Interfaces/MarketData/FallbackInterface.php @@ -20,6 +20,7 @@ public function __call($method, $arguments) $provider = trim($provider); try { + Log::warning("Calling method {$method} ({$provider})"); if (! in_array($provider, array_keys(config('investbrain.interfaces', [])))) { @@ -38,6 +39,13 @@ public function __call($method, $arguments) } } + // don't need to throw error if calling exists + if ($method == 'exists') { + + // symbol prob just doesn't exist + return false; + } + throw new \Exception("Could not get market data: {$this->latest_error}"); } } diff --git a/app/Interfaces/MarketData/FinnhubMarketData.php b/app/Interfaces/MarketData/FinnhubMarketData.php index 8c16554..8cd561e 100644 --- a/app/Interfaces/MarketData/FinnhubMarketData.php +++ b/app/Interfaces/MarketData/FinnhubMarketData.php @@ -28,17 +28,13 @@ public function __construct() public function exists(string $symbol): bool { - return $this->quote($symbol)->isNotEmpty(); + return (bool) $this->quote($symbol); } public function quote(string $symbol): Quote { $quote = $this->client->quote($symbol); - if (empty($quote)) { - return new Quote; - } - $fundamental = cache()->remember( 'fh-symbol-'.$symbol, 1440, diff --git a/app/Interfaces/MarketData/Types/Quote.php b/app/Interfaces/MarketData/Types/Quote.php index f8db045..ae49470 100644 --- a/app/Interfaces/MarketData/Types/Quote.php +++ b/app/Interfaces/MarketData/Types/Quote.php @@ -9,7 +9,7 @@ class Quote extends MarketDataType { - public function setName($name): self + public function setName(string $name): self { $this->items['name'] = (string) $name; @@ -21,7 +21,7 @@ public function getName(): string return $this->items['name'] ?? ''; } - public function setSymbol($symbol): self + public function setSymbol(string $symbol): self { $this->items['symbol'] = (string) $symbol; diff --git a/app/Interfaces/MarketData/YahooMarketData.php b/app/Interfaces/MarketData/YahooMarketData.php index cb3c48f..3261a2f 100644 --- a/app/Interfaces/MarketData/YahooMarketData.php +++ b/app/Interfaces/MarketData/YahooMarketData.php @@ -26,7 +26,7 @@ public function __construct() public function exists(string $symbol): bool { - return $this->quote($symbol)->isNotEmpty(); + return (bool) $this->quote($symbol); } public function quote(string $symbol): Quote @@ -34,22 +34,18 @@ public function quote(string $symbol): Quote $quote = $this->client->getQuote($symbol); - if (empty($quote)) { - return collect(); - } - return new Quote([ - 'name' => $quote->getLongName() ?? $quote->getShortName(), + 'name' => $quote?->getLongName() ?? $quote?->getShortName(), 'symbol' => $symbol, - '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(), - 'book_value' => $quote->getBookValue(), - 'last_dividend_date' => $quote->getDividendDate(), - 'dividend_yield' => $quote->getTrailingAnnualDividendYield() * 100, + '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(), + 'book_value' => $quote?->getBookValue(), + 'last_dividend_date' => $quote?->getDividendDate(), + 'dividend_yield' => $quote?->getTrailingAnnualDividendYield() * 100, ]); } diff --git a/app/Rules/SymbolValidationRule.php b/app/Rules/SymbolValidationRule.php index c01e844..428b6b9 100644 --- a/app/Rules/SymbolValidationRule.php +++ b/app/Rules/SymbolValidationRule.php @@ -29,12 +29,13 @@ public function validate(string $attribute, mixed $value, \Closure $fail): void { $this->symbol = $value; + // Check if the symbol exists in the Market Data table first (avoid API call) if (MarketData::find($this->symbol)) { return; } - // Check if the symbol exists in the Market Data table first (avoid API call) + // Then check against market data provider if (! app(MarketDataInterface::class)->exists($value)) { $fail('The symbol provided ('.$this->symbol.') is not valid'); } diff --git a/tests/FallbackInterfaceTest.php b/tests/FallbackInterfaceTest.php index 6053fe2..99282d2 100644 --- a/tests/FallbackInterfaceTest.php +++ b/tests/FallbackInterfaceTest.php @@ -77,4 +77,31 @@ public function test_all_providers_fail() Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed'); Log::shouldHaveReceived('warning')->with('Failed calling method quote (alpha): Alpha failed'); } + + public function test_exists_method_fails_without_exception() + { + config()->set('investbrain.provider', 'yahoo,alpha'); + config()->set('investbrain.interfaces', [ + 'yahoo' => YahooMarketData::class, + 'alphavantage' => AlphaVantageMarketData::class, + ]); + + $yahooMock = Mockery::mock(YahooMarketData::class); + $yahooMock->shouldReceive('exists') + ->andThrow(new \Exception('Yahoo failed')); + + $alphaMock = Mockery::mock(AlphaVantageMarketData::class); + $alphaMock->shouldReceive('exists') + ->andThrow(new \Exception('Alpha failed')); + + $this->app->instance(YahooMarketData::class, $yahooMock); + $this->app->instance(AlphaVantageMarketData::class, $alphaMock); + + $fallbackInterface = new FallbackInterface; + + $result = $fallbackInterface->exists('ZZZ'); + + $this->assertIsBool($result); + $this->assertFalse($result); + } }