From 729e02a209fd6b9b426ff86a85c8129b09a78447 Mon Sep 17 00:00:00 2001 From: compotypo Date: Thu, 14 Mar 2024 02:06:42 -0500 Subject: [PATCH] Added methods for getting, transforming stock options. Test to validate --- src/ApiClient.php | 19 +++++ src/ResultDecoder.php | 49 +++++++++++++ src/Results/Option.php | 111 +++++++++++++++++++++++++++++ tests/ApiClientIntegrationTest.php | 9 +++ 4 files changed, 188 insertions(+) create mode 100644 src/Results/Option.php diff --git a/src/ApiClient.php b/src/ApiClient.php index ea945f9..5f3b90d 100644 --- a/src/ApiClient.php +++ b/src/ApiClient.php @@ -285,6 +285,25 @@ public function stockSummary(string $symbol): array $responseBody = (string) $this->client->request('GET', $url, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody(); return $this->resultDecoder->transformQuotesSummary($responseBody); + } + + public function getStockOptions(string $symbol, ?\DateTimeInterface $expiryDate = null): array + { + $qs = $this->getRandomQueryServer(); + + // Initialize session cookies + $cookieJar = $this->getCookies(); + + // Get crumb value + $crumb = $this->getCrumb($qs, $cookieJar); + + // Fetch options + $url = 'https://query'.$qs.'.finance.yahoo.com/v7/finance/options/'.$symbol.'?crumb='.$crumb; + if ($expiryDate) { + $url .= '&date='.(string) $expiryDate->getTimestamp(); + } + $responseBody = (string) $this->client->request('GET', $url, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody(); + return $this->resultDecoder->transformOptions($responseBody); } } diff --git a/src/ResultDecoder.php b/src/ResultDecoder.php index 1677871..a3c93bb 100644 --- a/src/ResultDecoder.php +++ b/src/ResultDecoder.php @@ -8,6 +8,7 @@ use Scheb\YahooFinanceApi\Exception\InvalidValueException; use Scheb\YahooFinanceApi\Results\DividendData; use Scheb\YahooFinanceApi\Results\HistoricalData; +use Scheb\YahooFinanceApi\Results\Option; use Scheb\YahooFinanceApi\Results\Quote; use Scheb\YahooFinanceApi\Results\SearchResult; use Scheb\YahooFinanceApi\Results\SplitData; @@ -18,6 +19,23 @@ class ResultDecoder public const DIVIDEND_DATA_HEADER_LINE = ['Date', 'Dividends']; public const SPLIT_DATA_HEADER_LINE = ['Date', 'Stock Splits']; public const SEARCH_RESULT_FIELDS = ['symbol', 'name', 'exch', 'type', 'exchDisp', 'typeDisp']; + public const OPTION_FIELDS_MAP = [ + 'contractSymbol' => ValueMapperInterface::TYPE_STRING, + 'strike' => ValueMapperInterface::TYPE_FLOAT, + 'currency' => ValueMapperInterface::TYPE_STRING, + 'lastPrice' => ValueMapperInterface::TYPE_FLOAT, + 'change' => ValueMapperInterface::TYPE_FLOAT, + 'percentChange' => ValueMapperInterface::TYPE_FLOAT, + 'volume' => ValueMapperInterface::TYPE_INT, + 'openInterest' => ValueMapperInterface::TYPE_INT, + 'bid' => ValueMapperInterface::TYPE_FLOAT, + 'ask' => ValueMapperInterface::TYPE_FLOAT, + 'contractSize' => ValueMapperInterface::TYPE_STRING, + 'expiration' => ValueMapperInterface::TYPE_DATE, + 'lastTradeDate' => ValueMapperInterface::TYPE_DATE, + 'impliedVolatility' => ValueMapperInterface::TYPE_FLOAT, + 'inTheMoney' => ValueMapperInterface::TYPE_BOOL, + ]; public const QUOTE_FIELDS_MAP = [ 'ask' => ValueMapperInterface::TYPE_FLOAT, 'askSize' => ValueMapperInterface::TYPE_INT, @@ -283,4 +301,35 @@ public function transformQuotesSummary(string $responseBody): array return $decoded['quoteSummary']['result']; } + public function transformOptions(string $responseBody): array + { + $decoded = json_decode($responseBody, true); + if (!isset($decoded['optionChain']['result']) || !\is_array($decoded['optionChain']['result'])) { + throw new ApiException('Yahoo Search API returned an invalid result.', ApiException::INVALID_RESPONSE); + } + + $results = $decoded['optionChain']['result']; + + // Single element is returned directly in "quote" + return array_map(function (array $item) { + return $this->createOption($item); + }, $results); + } + + private function createOption(array $json): Option + { + $mappedValues = []; + foreach ($json as $field => $value) { + if (\array_key_exists($field, self::QUOTE_FIELDS_MAP)) { + $type = self::QUOTE_FIELDS_MAP[$field]; + try { + $mappedValues[$field] = $this->valueMapper->mapValue($value, $type); + } catch (InvalidValueException $e) { + throw new ApiException(sprintf('Not a %s in field "%s": %s', $type, $field, $value), ApiException::INVALID_VALUE, $e); + } + } + } + + return new Option($mappedValues); + } } diff --git a/src/Results/Option.php b/src/Results/Option.php new file mode 100644 index 0000000..0ef92a8 --- /dev/null +++ b/src/Results/Option.php @@ -0,0 +1,111 @@ + $value) { + $this->{$property} = $value; + } + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } + + public function getContractSymbol(): string + { + return $this->contractSymbol; + } + + public function getStrike(): float + { + return $this->strike; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getLastPrice(): float + { + return $this->lastPrice; + } + + public function getChange(): float + { + return $this->change; + } + + public function getPercentChange(): float + { + return $this->percentChange; + } + + public function getVolume(): int + { + return $this->volume; + } + + public function getOpenInterest(): int + { + return $this->openInterest; + } + + public function getBid(): float + { + return $this->bid; + } + + public function getAsk(): float + { + return $this->ask; + } + + public function getContractSize(): string + { + return $this->contractSize; + } + + public function getExpiration(): \DateTimeInterface + { + return $this->expiration; + } + + public function getLastTradeDate(): \DateTimeInterface + { + return $this->lastTradeDate; + } + + public function getImpliedVolatility(): float + { + return $this->impliedVolatility; + } + + public function getInTheMoney(): bool + { + return $this->inTheMoney; + } +} diff --git a/tests/ApiClientIntegrationTest.php b/tests/ApiClientIntegrationTest.php index 4261cc7..3322d27 100644 --- a/tests/ApiClientIntegrationTest.php +++ b/tests/ApiClientIntegrationTest.php @@ -10,6 +10,7 @@ use Scheb\YahooFinanceApi\ApiClientFactory; use Scheb\YahooFinanceApi\Results\DividendData; use Scheb\YahooFinanceApi\Results\HistoricalData; +use Scheb\YahooFinanceApi\Results\Option; use Scheb\YahooFinanceApi\Results\Quote; use Scheb\YahooFinanceApi\Results\SearchResult; use Scheb\YahooFinanceApi\Results\SplitData; @@ -317,4 +318,12 @@ public function testStockSummary(): void $this->assertEquals(self::APPLE_SYMBOL, $returnValue[0]['quoteType']['symbol']); } + public function testGetStockOptions(): void + { + $returnValue = $this->client->getStockOptions(self::APPLE_SYMBOL); + + $this->assertIsArray($returnValue); + $this->assertGreaterThan(0, \count($returnValue)); + $this->assertContainsOnlyInstancesOf(Option::class, $returnValue); + } }