diff --git a/README.md b/README.md index 94375a2..56c511a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ $quote = $client->getQuote("AAPL"); // Returns an array of Scheb\YahooFinanceApi\Results\Quote $quotes = $client->getQuotes(["AAPL", "GOOG"]); + +// Returns an array of Scheb\YahooFinanceApi\Results\OptionChain +$optionChain = $client->getOptionChain("AAPL"); +$optionChain = $client->getOptionChain("AAPL", new \DateTime("2021-01-01")); ``` Version Guidance diff --git a/src/ApiClient.php b/src/ApiClient.php index 3116858..8431f0c 100644 --- a/src/ApiClient.php +++ b/src/ApiClient.php @@ -197,6 +197,28 @@ public function getExchangeRates(array $currencyPairs): array return $this->fetchQuotes($currencySymbols); } + private function getCookies(): CookieJar + { + $cookieJar = new CookieJar(); + + // Initialize session cookies + $initialUrl = 'https://fc.yahoo.com'; + $this->client->request('GET', $initialUrl, ['cookies' => $cookieJar, 'http_errors' => false, 'headers' => $this->getHeaders()]); + + return $cookieJar; + } + + /** + * Get the crumb value from the Yahoo Finance API. + */ + private function getCrumb(int $qs, CookieJar $cookies): string + { + // Get crumb value + $initialUrl = 'https://query'.(string) $qs.'.finance.yahoo.com/v1/test/getcrumb'; + + return (string) $this->client->request('GET', $initialUrl, ['cookies' => $cookies, 'headers' => $this->getHeaders()])->getBody(); + } + /** * Fetch quote data from API. * @@ -205,15 +227,12 @@ public function getExchangeRates(array $currencyPairs): array private function fetchQuotes(array $symbols) { $qs = $this->getRandomQueryServer(); - $cookieJar = new CookieJar(); // Initialize session cookies - $initialUrl = 'https://fc.yahoo.com'; - $this->client->request('GET', $initialUrl, ['cookies' => $cookieJar, 'http_errors' => false, 'headers' => $this->getHeaders()]); + $cookieJar = $this->getCookies(); // Get crumb value - $initialUrl = 'https://query'.$qs.'.finance.yahoo.com/v1/test/getcrumb'; - $crumb = (string) $this->client->request('GET', $initialUrl, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody(); + $crumb = $this->getCrumb($qs, $cookieJar); // Fetch quotes $url = 'https://query'.$qs.'.finance.yahoo.com/v7/finance/quote?crumb='.$crumb.'&symbols='.urlencode(implode(',', $symbols)); @@ -253,21 +272,38 @@ private function getRandomQueryServer(): int public function stockSummary(string $symbol): array { $qs = $this->getRandomQueryServer(); - $cookieJar = new CookieJar(); // Initialize session cookies - $initialUrl = 'https://fc.yahoo.com'; - $this->client->request('GET', $initialUrl, ['cookies' => $cookieJar, 'http_errors' => false, 'headers' => $this->getHeaders()]); + $cookieJar = $this->getCookies(); // Get crumb value - $initialUrl = 'https://query'.$qs.'.finance.yahoo.com/v1/test/getcrumb'; - $crumb = (string) $this->client->request('GET', $initialUrl, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody(); + $crumb = $this->getCrumb($qs, $cookieJar); // Fetch quotes $modules = 'financialData,quoteType,defaultKeyStatistics,assetProfile,summaryDetail'; $url = 'https://query'.$qs.'.finance.yahoo.com/v10/finance/quoteSummary/'.$symbol.'?crumb='.$crumb.'&modules='.$modules; $responseBody = (string) $this->client->request('GET', $url, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody(); - return $this->resultDecoder->transformQuotesSumamary($responseBody); + return $this->resultDecoder->transformQuotesSummary($responseBody); + } + + public function getOptionChain(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->transformOptionChains($responseBody); } } diff --git a/src/ResultDecoder.php b/src/ResultDecoder.php index 4692b17..367e794 100644 --- a/src/ResultDecoder.php +++ b/src/ResultDecoder.php @@ -8,6 +8,9 @@ use Scheb\YahooFinanceApi\Exception\InvalidValueException; use Scheb\YahooFinanceApi\Results\DividendData; use Scheb\YahooFinanceApi\Results\HistoricalData; +use Scheb\YahooFinanceApi\Results\Option; +use Scheb\YahooFinanceApi\Results\OptionChain; +use Scheb\YahooFinanceApi\Results\OptionContract; use Scheb\YahooFinanceApi\Results\Quote; use Scheb\YahooFinanceApi\Results\SearchResult; use Scheb\YahooFinanceApi\Results\SplitData; @@ -18,6 +21,36 @@ 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_CHAIN_FIELDS_MAP = [ + 'underlyingSymbol' => ValueMapperInterface::TYPE_STRING, + 'expirationDates' => ValueMapperInterface::TYPE_ARRAY, + 'strikes' => ValueMapperInterface::TYPE_ARRAY, + 'hasMiniOptions' => ValueMapperInterface::TYPE_BOOL, + 'options' => ValueMapperInterface::TYPE_ARRAY, + ]; + public const OPTION_FIELDS_MAP = [ + 'expirationDate' => ValueMapperInterface::TYPE_DATE, + 'hasMiniOptions' => ValueMapperInterface::TYPE_BOOL, + 'calls' => ValueMapperInterface::TYPE_ARRAY, + 'puts' => ValueMapperInterface::TYPE_ARRAY, + ]; + public const OPTION_CONTRACT_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, @@ -273,7 +306,7 @@ private function createQuote(array $json): Quote return new Quote($mappedValues); } - public function transformQuotesSumamary(string $responseBody): array + public function transformQuotesSummary(string $responseBody): array { $decoded = json_decode($responseBody, true); if (!isset($decoded['quoteSummary']['result']) || !\is_array($decoded['quoteSummary']['result'])) { @@ -282,4 +315,98 @@ public function transformQuotesSumamary(string $responseBody): array return $decoded['quoteSummary']['result']; } + + public function transformOptionChains(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 "OptionChain" + $final = array_map(function (array $item) { + return $this->createOptionChain($item); + }, $results); + + return $final; + } + + private function createOptionChain(array $json): OptionChain + { + $mappedValues = []; + foreach ($json as $field => $value) { + if (!\array_key_exists($field, self::OPTION_CHAIN_FIELDS_MAP)) { + continue; + } + $type = self::OPTION_CHAIN_FIELDS_MAP[$field]; + try { + if ('options' === $field) { + if (!\is_array($value)) { + throw new InvalidValueException($type); + } + + $mappedValues[$field] = array_map(function (array $option): Option { + return $this->createOption($option); + }, $value); + } elseif ('expirationDates' === $field) { + $mappedValues[$field] = $this->valueMapper->mapValue($value, $type, ValueMapperInterface::TYPE_DATE); + } elseif ('strikes' === $field) { + $mappedValues[$field] = $this->valueMapper->mapValue($value, $type, ValueMapperInterface::TYPE_FLOAT); + } else { + $mappedValues[$field] = $this->valueMapper->mapValue($value, $type); + } + } catch (InvalidValueException $e) { + throw new ApiException(sprintf('%s in field "%s": %s', $e->getMessage(), $field, json_encode($value)), ApiException::INVALID_VALUE, $e); + } + } + + return new OptionChain($mappedValues); + } + + private function createOption(array $json): Option + { + $mappedValues = []; + foreach ($json as $field => $value) { + if (!\array_key_exists($field, self::OPTION_FIELDS_MAP)) { + continue; + } + $type = self::OPTION_FIELDS_MAP[$field]; + try { + if ('calls' === $field || 'puts' === $field) { + if (!\is_array($value)) { + throw new InvalidValueException($type); + } + + $mappedValues[$field] = array_map(function (array $optionContract): OptionContract { + return $this->createOptionContract($optionContract); + }, $value); + } else { + $mappedValues[$field] = $this->valueMapper->mapValue($value, $type); + } + } catch (InvalidValueException $e) { + throw new ApiException(sprintf('%s in field "%s": %s', $e->getMessage(), $field, json_encode($value)), ApiException::INVALID_VALUE, $e); + } + } + + return new Option($mappedValues); + } + + private function createOptionContract(array $values): OptionContract + { + $mappedValues = []; + foreach ($values as $property => $value) { + if (!\array_key_exists($property, self::OPTION_CONTRACT_FIELDS_MAP)) { + continue; + } + try { + $mappedValues[$property] = $this->valueMapper->mapValue($value, self::OPTION_CONTRACT_FIELDS_MAP[$property]); + } catch (InvalidValueException $e) { + throw new ApiException(sprintf('%s in field "%s": %s', $e->getMessage(), $property, json_encode($value)), ApiException::INVALID_VALUE, $e); + } + } + + return new OptionContract($mappedValues); + } } diff --git a/src/Results/Option.php b/src/Results/Option.php new file mode 100644 index 0000000..0d9751a --- /dev/null +++ b/src/Results/Option.php @@ -0,0 +1,54 @@ + $value) { + $this->{$property} = $value; + } + } + + public function jsonSerialize(): array + { + return [ + 'expirationDate' => $this->expirationDate, + 'hasMiniOptions' => $this->hasMiniOptions, + 'calls' => array_map(function (OptionContract $optionContract): array { + return $optionContract->jsonSerialize(); + }, $this->calls), + 'puts' => array_map(function (OptionContract $optionContract): array { + return $optionContract->jsonSerialize(); + }, $this->puts), + ]; + } + + public function getExpirationDate(): int + { + return $this->expirationDate; + } + + public function getHasMiniOptions(): bool + { + return $this->hasMiniOptions; + } + + public function getCalls(): array + { + return $this->calls; + } + + public function getPuts(): array + { + return $this->puts; + } +} diff --git a/src/Results/OptionChain.php b/src/Results/OptionChain.php new file mode 100644 index 0000000..dc9fc63 --- /dev/null +++ b/src/Results/OptionChain.php @@ -0,0 +1,59 @@ + $value) { + $this->{$property} = $value; + } + } + + public function jsonSerialize(): array + { + return [ + 'underlyingSymbol' => $this->underlyingSymbol, + 'expirationDates' => $this->expirationDates, + 'strikes' => $this->strikes, + 'hasMiniOptions' => $this->hasMiniOptions, + 'options' => array_map(function (Option $option): array { + return $option->jsonSerialize(); + }, $this->options), + ]; + } + + public function getUnderlyingSymbol(): string + { + return $this->underlyingSymbol; + } + + public function getExpirationDates(): array + { + return $this->expirationDates; + } + + public function getStrikes(): array + { + return $this->strikes; + } + + public function getHasMiniOptions(): bool + { + return $this->hasMiniOptions; + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/src/Results/OptionContract.php b/src/Results/OptionContract.php new file mode 100644 index 0000000..afdb23a --- /dev/null +++ b/src/Results/OptionContract.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/src/ValueMapper.php b/src/ValueMapper.php index be3e900..3b6db58 100644 --- a/src/ValueMapper.php +++ b/src/ValueMapper.php @@ -8,12 +8,27 @@ class ValueMapper implements ValueMapperInterface { + public function mapArray(array $rawValue, string $type): array + { + return array_map( + /** + * @param mixed $value + * + * @return mixed + */ + function ($value) use ($type) { + return $this->mapValue($value, $type); + }, + $rawValue + ); + } + /** * @param mixed $rawValue * * @return mixed */ - public function mapValue($rawValue, string $type) + public function mapValue($rawValue, string $type, ?string $subType = null) { if (null === $rawValue) { return null; @@ -30,6 +45,12 @@ public function mapValue($rawValue, string $type) return (string) $rawValue; case self::TYPE_BOOL: return $this->mapBoolValue($rawValue); + case self::TYPE_ARRAY: + if (null === $subType) { + throw new \InvalidArgumentException('Subtype must be provided for array type'); + } + + return $this->mapArray($rawValue, $subType); default: throw new \InvalidArgumentException(sprintf('Invalid data type %s', $type)); } @@ -64,7 +85,15 @@ private function mapIntValue($rawValue): int */ private function mapBoolValue($rawValue): bool { - return (bool) $rawValue; + if (is_numeric($rawValue)) { + return (bool) $rawValue; + } + + if (!\is_bool($rawValue)) { + throw new InvalidValueException(ValueMapperInterface::TYPE_BOOL); + } + + return $rawValue; } /** diff --git a/src/ValueMapperInterface.php b/src/ValueMapperInterface.php index ddcfaa4..b229e88 100644 --- a/src/ValueMapperInterface.php +++ b/src/ValueMapperInterface.php @@ -6,16 +6,20 @@ interface ValueMapperInterface { + public const TYPE_ARRAY = 'array'; + public const TYPE_OBJECT = 'object'; public const TYPE_FLOAT = 'float'; public const TYPE_INT = 'int'; public const TYPE_DATE = 'date'; public const TYPE_STRING = 'string'; public const TYPE_BOOL = 'bool'; + public function mapArray(array $rawValue, string $type): array; + /** * @param mixed $rawValue * * @return mixed */ - public function mapValue($rawValue, string $type); + public function mapValue($rawValue, string $type, ?string $subType = null); } diff --git a/tests/ApiClientIntegrationTest.php b/tests/ApiClientIntegrationTest.php index ef1aaa0..b430483 100644 --- a/tests/ApiClientIntegrationTest.php +++ b/tests/ApiClientIntegrationTest.php @@ -10,6 +10,9 @@ use Scheb\YahooFinanceApi\ApiClientFactory; use Scheb\YahooFinanceApi\Results\DividendData; use Scheb\YahooFinanceApi\Results\HistoricalData; +use Scheb\YahooFinanceApi\Results\Option; +use Scheb\YahooFinanceApi\Results\OptionChain; +use Scheb\YahooFinanceApi\Results\OptionContract; use Scheb\YahooFinanceApi\Results\Quote; use Scheb\YahooFinanceApi\Results\SearchResult; use Scheb\YahooFinanceApi\Results\SplitData; @@ -309,11 +312,50 @@ public function runBare(): void } } - public function testStockSummary() + public function testStockSummary(): void { $returnValue = $this->client->stockSummary(self::APPLE_SYMBOL); $this->assertIsArray($returnValue); $this->assertEquals(self::APPLE_SYMBOL, $returnValue[0]['quoteType']['symbol']); } + + public function testgetOptionChain(): void + { + $returnValue = $this->client->getOptionChain(self::APPLE_SYMBOL); + + $this->assertIsArray($returnValue); + $this->assertGreaterThan(0, \count($returnValue)); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnValue); + foreach ($returnValue as $optionChain) { + $options = $optionChain->getOptions(); + $this->assertContainsOnlyInstancesOf(Option::class, $options); + foreach ($options as $option) { + $calls = $option->getCalls(); + $this->assertContainsOnlyInstancesOf(OptionContract::class, $calls); + $puts = $option->getPuts(); + $this->assertContainsOnlyInstancesOf(OptionContract::class, $puts); + } + } + } + + public function testGetStockOptions_historicExpiryDate(): void + { + $returnValue = $this->client->getOptionChain(self::APPLE_SYMBOL, new \DateTime('2024-01-04')); + + $this->assertIsArray($returnValue); + $this->assertGreaterThan(0, \count($returnValue)); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnValue); + + foreach ($returnValue as $optionChain) { + $options = $optionChain->getOptions(); + $this->assertContainsOnlyInstancesOf(Option::class, $options); + foreach ($options as $option) { + $calls = $option->getCalls(); + $this->assertContainsOnlyInstancesOf(OptionContract::class, $calls); + $puts = $option->getPuts(); + $this->assertContainsOnlyInstancesOf(OptionContract::class, $puts); + } + } + } } diff --git a/tests/ResultDecoderTest.php b/tests/ResultDecoderTest.php index b2854de..70accc4 100644 --- a/tests/ResultDecoderTest.php +++ b/tests/ResultDecoderTest.php @@ -9,6 +9,7 @@ use Scheb\YahooFinanceApi\ResultDecoder; use Scheb\YahooFinanceApi\Results\DividendData; use Scheb\YahooFinanceApi\Results\HistoricalData; +use Scheb\YahooFinanceApi\Results\OptionChain; use Scheb\YahooFinanceApi\Results\Quote; use Scheb\YahooFinanceApi\Results\SearchResult; use Scheb\YahooFinanceApi\Results\SplitData; @@ -540,4 +541,444 @@ public function transformSearchResult_jsonWithMissedFieldGiven_createSearchResul $this->resultDecoder->transformSearchResult(json_encode($jsonArray)); } + + /** + * @test + * @dataProvider transformQuotesInvalidResult + */ + public function transformOptionChains_jsonGiven_createArrayOfInvalidResult($responseBody): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Yahoo Search API returned an invalid result'); + + $this->resultDecoder->transformOptionChains(json_encode($responseBody)); + } + + /** + * @test + */ + public function transformOptionChains_jsonGiven_createArrayOfOptionChain(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/optionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => 'AAPL', + 'expirationDates' => [ + new \DateTime('@1711065600'), + new \DateTime('@1711584000'), + new \DateTime('@1781740800'), + ], + 'strikes' => [ + 100.0, + 105.0, + 265.0, + ], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => new \DateTime('@1711065600'), + 'hasMiniOptions' => false, + 'calls' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 256.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + 'puts' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 265.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithNullGiven_createArrayOfOptionChain(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/nullOptionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => null, + 'expirationDates' => [], + 'strikes' => [], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => null, + 'hasMiniOptions' => false, + 'calls' => [], + 'puts' => [], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidFloatGivenInOptionChain_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a float in field "strikes": ["invalid_float",105,265]'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidFloatOptionChain.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidDateTimeGivenInOptionsChain_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a date in field "expirationDates": ["invalid_date_time",1711584000,1781740800]'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidDateTimeOptionChain.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidBooleanGivenInOptionsChain_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a bool in field "hasMiniOptions": "invalid_boolean"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidBooleanOptionChain.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidArrayGivenInOptionsChain_apiExceptionThrownForInvalidData(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a array in field "options": "invalid_array"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidArrayOptionChain.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonGiven_createArrayOfOptions(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/optionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => 'AAPL', + 'expirationDates' => [ + new \DateTime('@1711065600'), + new \DateTime('@1711584000'), + new \DateTime('@1781740800'), + ], + 'strikes' => [ + 100.0, + 105.0, + 265.0, + ], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => new \DateTime('@1711065600'), + 'hasMiniOptions' => false, + 'calls' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 256.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + 'puts' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 265.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithNullGiven_HandleNullResult(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/nullOptionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => null, + 'expirationDates' => [], + 'strikes' => [], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => null, + 'hasMiniOptions' => false, + 'calls' => [], + 'puts' => [], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidArrayGivenInOption_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a array in field "calls": ""'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidArrayOption.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidDateTimeGivenInOption_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a date in field "expirationDate": "invalid_date_time"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidDateTimeOption.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidBooleanGivenInOption_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a bool in field "hasMiniOptions": "invalid_boolean"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidBooleanOption.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonGiven_createArrayOfOptionContracts(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/optionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => 'AAPL', + 'expirationDates' => [ + new \DateTime('@1711065600'), + new \DateTime('@1711584000'), + new \DateTime('@1781740800'), + ], + 'strikes' => [ + 100.0, + 105.0, + 265.0, + ], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => new \DateTime('@1711065600'), + 'hasMiniOptions' => false, + 'calls' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 256.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + 'puts' => [ + [ + 'contractSymbol' => 'AAPL240322P00265000', + 'strike' => 265.0, + 'currency' => 'USD', + 'lastPrice' => 93.65, + 'change' => 6.7699966, + 'percentChange' => 7.7744565, + 'volume' => 3, + 'openInterest' => 0, + 'bid' => 90.65, + 'ask' => 94.8, + 'contractSize' => 'REGULAR', + 'expiration' => new \DateTime('@1598590800'), + 'lastTradeDate' => new \DateTime('@1597899600'), + 'impliedVolatility' => 1.642579912109375, + 'inTheMoney' => false, + ], + ], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithNullGiven_createArrayOfOptionContracts(): void + { + $returnedResult = $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/nullOptionChain.json')); + + $this->assertIsArray($returnedResult); + $this->assertCount(1, $returnedResult); + $this->assertContainsOnlyInstancesOf(OptionChain::class, $returnedResult); + + $expectedOptionChainData = [ + [ + 'underlyingSymbol' => null, + 'expirationDates' => [], + 'strikes' => [], + 'hasMiniOptions' => false, + 'options' => [ + [ + 'expirationDate' => null, + 'hasMiniOptions' => false, + 'calls' => [], + 'puts' => [], + ], + ], + ], + ]; + + $this->assertEquals($expectedOptionChainData[0], $returnedResult[0]->jsonSerialize()); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidFloatGiven_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a float in field "percentChange": "7.7744565%"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidFloatOptionContract.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidDateTimeGiven_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a date in field "expiration": "invalid_date_time"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidDateTimeOptionContract.json')); + } + + /** + * @test + */ + public function transformOptionChains_jsonWithInvalidBooleanGiven_apiExceptionThrown(): void + { + $this->expectException(ApiException::class); + $this->expectExceptionMessage('Not a bool in field "inTheMoney": "invalid_boolean"'); + + $this->resultDecoder->transformOptionChains(file_get_contents(__DIR__.'/fixtures/invalidBooleanOptionContract.json')); + } } diff --git a/tests/fixtures/invalidArrayOption.json b/tests/fixtures/invalidArrayOption.json new file mode 100644 index 0000000..02fa5ee --- /dev/null +++ b/tests/fixtures/invalidArrayOption.json @@ -0,0 +1,47 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": "", + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidArrayOptionChain.json b/tests/fixtures/invalidArrayOptionChain.json new file mode 100644 index 0000000..6220c2c --- /dev/null +++ b/tests/fixtures/invalidArrayOptionChain.json @@ -0,0 +1,22 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": "invalid_array" + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidBooleanOption.json b/tests/fixtures/invalidBooleanOption.json new file mode 100644 index 0000000..f8eb1c3 --- /dev/null +++ b/tests/fixtures/invalidBooleanOption.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": "invalid_boolean", + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": "invalid_boolean" + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidBooleanOptionChain.json b/tests/fixtures/invalidBooleanOptionChain.json new file mode 100644 index 0000000..599bf65 --- /dev/null +++ b/tests/fixtures/invalidBooleanOptionChain.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": "invalid_boolean", + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": "invalid_boolean" + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidBooleanOptionContract.json b/tests/fixtures/invalidBooleanOptionContract.json new file mode 100644 index 0000000..4ca06c3 --- /dev/null +++ b/tests/fixtures/invalidBooleanOptionContract.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": "invalid_boolean" + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidDateTimeOption.json b/tests/fixtures/invalidDateTimeOption.json new file mode 100644 index 0000000..9d10fc7 --- /dev/null +++ b/tests/fixtures/invalidDateTimeOption.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1700541200, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": "invalid_date_time", + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": "invalid_date_time", + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidDateTimeOptionChain.json b/tests/fixtures/invalidDateTimeOptionChain.json new file mode 100644 index 0000000..54d4074 --- /dev/null +++ b/tests/fixtures/invalidDateTimeOptionChain.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + "invalid_date_time", + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": "invalid_date_time", + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidDateTimeOptionContract.json b/tests/fixtures/invalidDateTimeOptionContract.json new file mode 100644 index 0000000..9397e6f --- /dev/null +++ b/tests/fixtures/invalidDateTimeOptionContract.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": "invalid_date_time", + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidFloatOptionChain.json b/tests/fixtures/invalidFloatOptionChain.json new file mode 100644 index 0000000..e2d3681 --- /dev/null +++ b/tests/fixtures/invalidFloatOptionChain.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + "invalid_float", + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": "7.7744565%", + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/invalidFloatOptionContract.json b/tests/fixtures/invalidFloatOptionContract.json new file mode 100644 index 0000000..6532fe5 --- /dev/null +++ b/tests/fixtures/invalidFloatOptionContract.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": "7.7744565%", + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/nullOptionChain.json b/tests/fixtures/nullOptionChain.json new file mode 100644 index 0000000..ec6a262 --- /dev/null +++ b/tests/fixtures/nullOptionChain.json @@ -0,0 +1,21 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": null, + "expirationDates": [], + "strikes": [], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": null, + "hasMiniOptions": false, + "calls": [], + "puts": [] + } + ] + } + ], + "error": null + } +} \ No newline at end of file diff --git a/tests/fixtures/optionChain.json b/tests/fixtures/optionChain.json new file mode 100644 index 0000000..f65e4b9 --- /dev/null +++ b/tests/fixtures/optionChain.json @@ -0,0 +1,65 @@ +{ + "optionChain": { + "result": [ + { + "underlyingSymbol": "AAPL", + "expirationDates": [ + 1711065600, + 1711584000, + 1781740800 + ], + "strikes": [ + 100.0, + 105.0, + 265.0 + ], + "hasMiniOptions": false, + "options": [ + { + "expirationDate": 1711065600, + "hasMiniOptions": false, + "calls": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 256.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ], + "puts": [ + { + "contractSymbol": "AAPL240322P00265000", + "strike": 265.0, + "currency": "USD", + "lastPrice": 93.65, + "change": 6.7699966, + "percentChange": 7.7744565, + "volume": 3, + "openInterest": 0, + "bid": 90.65, + "ask": 94.8, + "contractSize": "REGULAR", + "expiration": 1598590800, + "lastTradeDate": 1597899600, + "impliedVolatility": 1.642579912109375, + "inTheMoney": false + } + ] + } + ] + } + ], + "error": null + } +} \ No newline at end of file