Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

correct issue getHistoricalQuoteData-start-to-give-401-unauthorized-#52 #54

Merged
merged 5 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,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 @@ -118,7 +118,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 @@ -140,7 +140,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 @@ -241,10 +241,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 @@ -195,81 +195,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
Loading