Skip to content

Commit

Permalink
correct issue getHistoricalQuoteData-start-to-give-401-unauthorized-s…
Browse files Browse the repository at this point in the history
…cheb#52 (scheb#54)

Fix issue getHistoricalQuoteData start to give 401 unauthorized scheb#52
  • Loading branch information
jrodriguesd authored and krivov committed Jan 16, 2025
1 parent 0da7298 commit 220832b
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 114 deletions.
10 changes: 5 additions & 5 deletions src/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public function getHistoricalQuoteData(string $symbol, string $interval, \DateTi
$this->validateIntervals($interval);
$this->validateDates($startDate, $endDate);

$responseBody = $this->getHistoricalDataResponseBody($symbol, $interval, $startDate, $endDate, self::FILTER_HISTORICAL);
$responseBody = $this->getHistoricalDataResponseBodyJson($symbol, $interval, $startDate, $endDate, self::FILTER_HISTORICAL);

return $this->resultDecoder->transformHistoricalDataResult($responseBody);
}
Expand All @@ -119,7 +119,7 @@ public function getHistoricalDividendData(string $symbol, \DateTimeInterface $st
{
$this->validateDates($startDate, $endDate);

$responseBody = $this->getHistoricalDataResponseBody($symbol, self::INTERVAL_1_MONTH, $startDate, $endDate, self::FILTER_DIVIDENDS);
$responseBody = $this->getHistoricalDataResponseBodyJson($symbol, self::INTERVAL_1_MONTH, $startDate, $endDate, self::FILTER_DIVIDENDS);

$historicData = $this->resultDecoder->transformDividendDataResult($responseBody);
usort($historicData, function (DividendData $a, DividendData $b): int {
Expand All @@ -141,7 +141,7 @@ public function getHistoricalSplitData(string $symbol, \DateTimeInterface $start
{
$this->validateDates($startDate, $endDate);

$responseBody = $this->getHistoricalDataResponseBody($symbol, self::INTERVAL_1_MONTH, $startDate, $endDate, self::FILTER_SPLITS);
$responseBody = $this->getHistoricalDataResponseBodyJson($symbol, self::INTERVAL_1_MONTH, $startDate, $endDate, self::FILTER_SPLITS);

$historicData = $this->resultDecoder->transformSplitDataResult($responseBody);
usort($historicData, function (SplitData $a, SplitData $b): int {
Expand Down Expand Up @@ -242,10 +242,10 @@ private function fetchQuotes(array $symbols)
return $this->resultDecoder->transformQuotes($responseBody);
}

private function getHistoricalDataResponseBody(string $symbol, string $interval, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $filter): string
private function getHistoricalDataResponseBodyJson(string $symbol, string $interval, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $filter): string
{
$qs = $this->getRandomQueryServer();
$dataUrl = 'https://query'.$qs.'.finance.yahoo.com/v7/finance/download/'.urlencode($symbol).'?period1='.$startDate->getTimestamp().'&period2='.$endDate->getTimestamp().'&interval='.$interval.'&events='.$filter;
$dataUrl = 'https://query'.$qs.'.finance.yahoo.com/v8/finance/chart/'.urlencode($symbol).'?period1='.$startDate->getTimestamp().'&period2='.$endDate->getTimestamp().'&interval='.$interval.'&events='.$filter;

return (string) $this->client->request('GET', $dataUrl, ['headers' => $this->getHeaders()])->getBody();
}
Expand Down
104 changes: 62 additions & 42 deletions src/ResultDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,81 +217,101 @@ private function validateDate(string $value): \DateTime

public function transformHistoricalDataResult(string $responseBody): array
{
$lines = $this->validateHeaderLines($responseBody, self::HISTORICAL_DATA_HEADER_LINE);
$decoded = json_decode($responseBody, true);

if ((!\is_array($decoded)) || (isset($decoded['chart']['error']))) {
throw new ApiException('Response is not a valid JSON', ApiException::INVALID_RESPONSE);
}

$result = $decoded['chart']['result'][0];
$entryCount = \count($result['timestamp']);
$returnArray = [];
for ($i = 0; $i < $entryCount; ++$i) {
$returnArray[] = $this->createHistoricalData($result, $i);
}

return array_map(function ($line) {
return $this->createHistoricalData(explode(',', $line));
}, $lines);
return $returnArray;
}

private function createHistoricalData(array $columns): HistoricalData
private function createHistoricalData(array $json, int $index): HistoricalData
{
if (7 !== \count($columns)) {
throw new ApiException('CSV did not contain correct number of columns', ApiException::INVALID_RESPONSE);
$dateStr = date('Y-m-d', $json['timestamp'][$index]);
if ($dateStr) {
$date = $this->validateDate($dateStr);
} else {
throw new ApiException(\sprintf('Not a date in column "Date":%s', $json['timestamp'][$index]), ApiException::INVALID_VALUE);
}

$date = $this->validateDate($columns[0]);

for ($i = 1; $i <= 6; ++$i) {
if (!is_numeric($columns[$i]) && 'null' !== $columns[$i]) {
throw new ApiException(sprintf('Not a number in column "%s": %s', self::HISTORICAL_DATA_HEADER_LINE[$i], $columns[$i]), ApiException::INVALID_VALUE);
foreach (['open', 'high', 'low', 'close', 'volume'] as $column) {
$columnValue = $json['indicators']['quote'][0][$column][$index];
if (!is_numeric($columnValue) && 'null' !== $columnValue) {
throw new ApiException(\sprintf('Not a number in column "%s": %s', $column, $column), ApiException::INVALID_VALUE);
}
}

$open = (float) $columns[1];
$high = (float) $columns[2];
$low = (float) $columns[3];
$close = (float) $columns[4];
$adjClose = (float) $columns[5];
$volume = (int) $columns[6];
$columnValue = $json['indicators']['adjclose'][0]['adjclose'][$index];
if (!is_numeric($columnValue) && 'null' !== $columnValue) {
throw new ApiException(\sprintf('Not a number in column "%s": %s', 'adjclose', 'adjclose'), ApiException::INVALID_VALUE);
}

$open = (float) $json['indicators']['quote'][0]['open'][$index];
$high = (float) $json['indicators']['quote'][0]['high'][$index];
$low = (float) $json['indicators']['quote'][0]['low'][$index];
$close = (float) $json['indicators']['quote'][0]['close'][$index];
$volume = (int) $json['indicators']['quote'][0]['volume'][$index];
$adjClose = (float) $json['indicators']['adjclose'][0]['adjclose'][$index];

return new HistoricalData($date, $open, $high, $low, $close, $adjClose, $volume);
}

public function transformDividendDataResult(string $responseBody): array
{
$lines = $this->validateHeaderLines($responseBody, self::DIVIDEND_DATA_HEADER_LINE);
$decoded = json_decode($responseBody, true);
if ((!\is_array($decoded)) || (isset($decoded['chart']['error']))) {
throw new ApiException('Response is not a valid JSON', ApiException::INVALID_RESPONSE);
}

return array_map(function ($line) {
return $this->createDividendData(explode(',', $line));
}, $lines);
return array_map(function (array $item) {
return $this->createDividendData($item);
}, $decoded['chart']['result'][0]['events']['dividends']);
}

private function createDividendData(array $columns): DividendData
private function createDividendData(array $json): DividendData
{
if (2 !== \count($columns)) {
throw new ApiException('CSV did not contain correct number of columns', ApiException::INVALID_RESPONSE);
}

$date = $this->validateDate($columns[0]);

if (!is_numeric($columns[1]) && 'null' !== $columns[1]) {
throw new ApiException(sprintf('Not a number in column Dividends: %s', $columns[1]), ApiException::INVALID_VALUE);
$dateStr = date('Y-m-d', $json['date']);
if ($dateStr) {
$date = $this->validateDate($dateStr);
} else {
throw new ApiException(\sprintf('Not a date in column "Date":%s', $json['date']), ApiException::INVALID_VALUE);
}

$dividends = (float) $columns[1];
$dividends = (float) $json['amount'];

return new DividendData($date, $dividends);
}

public function transformSplitDataResult(string $responseBody): array
{
$lines = $this->validateHeaderLines($responseBody, self::SPLIT_DATA_HEADER_LINE);
$decoded = json_decode($responseBody, true);
if ((!\is_array($decoded)) || (isset($decoded['chart']['error']))) {
throw new ApiException('Response is not a valid JSON', ApiException::INVALID_RESPONSE);
}

return array_map(function ($line) {
return $this->createSplitData(explode(',', $line));
}, $lines);
return array_map(function (array $item) {
return $this->createSplitData($item);
}, $decoded['chart']['result'][0]['events']['splits']);
}

private function createSplitData(array $columns): SplitData
private function createSplitData(array $json): SplitData
{
if (2 !== \count($columns)) {
throw new ApiException('CSV did not contain correct number of columns', ApiException::INVALID_RESPONSE);
$dateStr = date('Y-m-d', $json['date']);
if ($dateStr) {
$date = $this->validateDate($dateStr);
} else {
throw new ApiException(\sprintf('Not a date in column "Date":%s', $json['date']), ApiException::INVALID_VALUE);
}

$date = $this->validateDate($columns[0]);

$stockSplits = (string) $columns[1];
$stockSplits = (string) $json['splitRatio'];

return new SplitData($date, $stockSplits);
}
Expand Down
54 changes: 28 additions & 26 deletions tests/ResultDecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,19 @@ public function extractCrumb_invalidStringGiven_throwApiException(): void
*/
public function transformHistoricalDataResult_csvGiven_returnArrayOfHistoricalData(): void
{
$returnedResult = $this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/historicalData.csv'));
$returnedResult = $this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/historicalData.json'));

$this->assertIsArray($returnedResult);
$this->assertContainsOnlyInstancesOf(HistoricalData::class, $returnedResult);

$expectedExchangeRate = new HistoricalData(
new \DateTime('2017-07-11'),
144.729996,
145.850006,
144.380005,
145.529999,
145.529999,
19781800
new \DateTime('2024-09-30', new \DateTimeZone('UTC')),
230.0399932861328,
233.0,
229.64999389648438,
233.0,
233.0,
54541900
);
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
}
Expand All @@ -129,7 +129,7 @@ public function transformHistoricalDataResult_csvGiven_returnArrayOfHistoricalDa
public function transformHistoricalDataResult_invalidColumnsCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV did not contain correct number of columns');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/invalidColumnsHistoricalData.csv'));
}
Expand All @@ -140,7 +140,7 @@ public function transformHistoricalDataResult_invalidColumnsCsvGiven_throwApiExc
public function transformHistoricalDataResult_unexpectedHeaderLineCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV header line did not match expected header line, given: 12345 1234567, expected: Date,Open,High,Low,Close,Adj Close,Volume');
$this->expectExceptionMessage('Response is not a valid JSON');

$invalidCsvString = "12345\t1234567\t";
$this->resultDecoder->transformHistoricalDataResult($invalidCsvString);
Expand All @@ -152,7 +152,7 @@ public function transformHistoricalDataResult_unexpectedHeaderLineCsvGiven_throw
public function transformHistoricalDataResult_invalidDateTimeFormatCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Not a date in column "Date":2017-07');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/invalidDateTimeFormatHistoricalData.csv'));
}
Expand All @@ -163,7 +163,7 @@ public function transformHistoricalDataResult_invalidDateTimeFormatCsvGiven_thro
public function transformHistoricalDataResult_invalidNumericStringCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Not a number in column "High": this_is_not_numeric_string');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/invalidNumericStringHistoricalData.csv'));
}
Expand All @@ -173,16 +173,17 @@ public function transformHistoricalDataResult_invalidNumericStringCsvGiven_throw
*/
public function transformDividendDataResult_csvGiven_returnArrayOfDividendData(): void
{
$returnedResult = $this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/dividendData.csv'));
$returnedResult = $this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/dividendData.json'));

$this->assertIsArray($returnedResult);
$this->assertContainsOnlyInstancesOf(DividendData::class, $returnedResult);
$firstResult = array_shift($returnedResult);

$expectedExchangeRate = new DividendData(
new \DateTime('2017-07-11'),
0.205
new \DateTime('2019-11-07', new \DateTimeZone('UTC')),
0.1925
);
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
$this->assertEquals($expectedExchangeRate, $firstResult);
}

/**
Expand All @@ -191,7 +192,7 @@ public function transformDividendDataResult_csvGiven_returnArrayOfDividendData()
public function transformDividendDataResult_invalidColumnsCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV did not contain correct number of columns');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/invalidColumnsDividendData.csv'));
}
Expand All @@ -202,7 +203,7 @@ public function transformDividendDataResult_invalidColumnsCsvGiven_throwApiExcep
public function transformDividendDataResult_unexpectedHeaderLineCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV header line did not match expected header line, given: 12345 1234567, expected: Date,Dividends');
$this->expectExceptionMessage('Response is not a valid JSON');

$invalidCsvString = "12345\t1234567\t";
$this->resultDecoder->transformDividendDataResult($invalidCsvString);
Expand All @@ -214,7 +215,7 @@ public function transformDividendDataResult_unexpectedHeaderLineCsvGiven_throwAp
public function transformDividendDataResult_invalidDateTimeFormatCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Not a date in column "Date":2017-07');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/invalidDateTimeFormatDividendData.csv'));
}
Expand All @@ -225,7 +226,7 @@ public function transformDividendDataResult_invalidDateTimeFormatCsvGiven_throwA
public function transformDividendDataResult_invalidNumericStringCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Not a number in column Dividends: this_is_not_numeric_string');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/invalidNumericStringDividendData.csv'));
}
Expand All @@ -235,16 +236,17 @@ public function transformDividendDataResult_invalidNumericStringCsvGiven_throwAp
*/
public function transformSplitDataResult_csvGiven_returnArrayOfSplitData(): void
{
$returnedResult = $this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/splitData.csv'));
$returnedResult = $this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/splitData.json'));

$this->assertIsArray($returnedResult);
$this->assertContainsOnlyInstancesOf(SplitData::class, $returnedResult);
$firstResult = array_shift($returnedResult);

$expectedExchangeRate = new SplitData(
new \DateTime('2017-07-11'),
new \DateTime('2020-08-31', new \DateTimeZone('UTC')),
'4:1'
);
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
$this->assertEquals($expectedExchangeRate, $firstResult);
}

/**
Expand All @@ -253,7 +255,7 @@ public function transformSplitDataResult_csvGiven_returnArrayOfSplitData(): void
public function transformSplitDataResult_invalidColumnsCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV did not contain correct number of columns');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/invalidColumnsSplitData.csv'));
}
Expand All @@ -264,7 +266,7 @@ public function transformSplitDataResult_invalidColumnsCsvGiven_throwApiExceptio
public function transformSplitDataResult_unexpectedHeaderLineCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('CSV header line did not match expected header line, given: 12345 1234567, expected: Date,Stock Splits');
$this->expectExceptionMessage('Response is not a valid JSON');

$invalidCsvString = "12345\t1234567\t";
$this->resultDecoder->transformSplitDataResult($invalidCsvString);
Expand All @@ -276,7 +278,7 @@ public function transformSplitDataResult_unexpectedHeaderLineCsvGiven_throwApiEx
public function transformSplitDataResult_invalidDateTimeFormatCsvGiven_throwApiException(): void
{
$this->expectException(ApiException::class);
$this->expectExceptionMessage('Not a date in column "Date":2017-07');
$this->expectExceptionMessage('Response is not a valid JSON');

$this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/invalidDateTimeFormatSplitData.csv'));
}
Expand Down
11 changes: 0 additions & 11 deletions tests/fixtures/dividendData.csv

This file was deleted.

Loading

0 comments on commit 220832b

Please sign in to comment.