Skip to content

Commit

Permalink
Added additional testing for options. Proofed entire option chain model.
Browse files Browse the repository at this point in the history
  • Loading branch information
CompoTypo committed Mar 30, 2024
1 parent 729e02a commit d58d79f
Show file tree
Hide file tree
Showing 22 changed files with 1,471 additions and 96 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/ApiClient.php
Expand Up @@ -287,7 +287,7 @@ public function stockSummary(string $symbol): array
return $this->resultDecoder->transformQuotesSummary($responseBody);
}

public function getStockOptions(string $symbol, ?\DateTimeInterface $expiryDate = null): array
public function getOptionChain(string $symbol, ?\DateTimeInterface $expiryDate = null): array
{
$qs = $this->getRandomQueryServer();

Expand All @@ -304,6 +304,6 @@ public function getStockOptions(string $symbol, ?\DateTimeInterface $expiryDate
}
$responseBody = (string) $this->client->request('GET', $url, ['cookies' => $cookieJar, 'headers' => $this->getHeaders()])->getBody();

return $this->resultDecoder->transformOptions($responseBody);
return $this->resultDecoder->transformOptionChains($responseBody);
}
}
95 changes: 86 additions & 9 deletions src/ResultDecoder.php
Expand Up @@ -9,6 +9,8 @@
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;
Expand All @@ -19,7 +21,20 @@ 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,
Expand Down Expand Up @@ -301,7 +316,7 @@ public function transformQuotesSummary(string $responseBody): array
return $decoded['quoteSummary']['result'];
}

public function transformOptions(string $responseBody): array
public function transformOptionChains(string $responseBody): array
{
$decoded = json_decode($responseBody, true);
if (!isset($decoded['optionChain']['result']) || !\is_array($decoded['optionChain']['result'])) {
Expand All @@ -310,26 +325,88 @@ public function transformOptions(string $responseBody): array

$results = $decoded['optionChain']['result'];

// Single element is returned directly in "quote"
return array_map(function (array $item) {
return $this->createOption($item);
// 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::QUOTE_FIELDS_MAP)) {
$type = self::QUOTE_FIELDS_MAP[$field];
try {
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('Not a %s in field "%s": %s', $type, $field, $value), ApiException::INVALID_VALUE, $e);
}
} 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);
}
}
101 changes: 22 additions & 79 deletions src/Results/Option.php
Expand Up @@ -6,21 +6,10 @@

class Option implements \JsonSerializable
{
private $contractSymbol;
private $strike;
private $currency;
private $lastPrice;
private $change;
private $percentChange;
private $volume;
private $openInterest;
private $bid;
private $ask;
private $contractSize;
private $expiration;
private $lastTradeDate;
private $impliedVolatility;
private $inTheMoney;
private $expirationDate;
private $hasMiniOptions;
private $calls;
private $puts;

public function __construct(array $values)
{
Expand All @@ -31,81 +20,35 @@ public function __construct(array $values)

public function jsonSerialize(): array
{
return get_object_vars($this);
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 getContractSymbol(): string
public function getExpirationDate(): int
{
return $this->contractSymbol;
return $this->expirationDate;
}

public function getStrike(): float
public function getHasMiniOptions(): bool
{
return $this->strike;
return $this->hasMiniOptions;
}

public function getCurrency(): string
public function getCalls(): array
{
return $this->currency;
return $this->calls;
}

public function getLastPrice(): float
public function getPuts(): array
{
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;
return $this->puts;
}
}
59 changes: 59 additions & 0 deletions src/Results/OptionChain.php
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Scheb\YahooFinanceApi\Results;

class OptionChain implements \JsonSerializable
{
private $underlyingSymbol;
private $expirationDates;
private $strikes;
private $hasMiniOptions;
private $options;

public function __construct(array $values)
{
foreach ($values as $property => $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;
}
}

0 comments on commit d58d79f

Please sign in to comment.