Skip to content

Commit 34ec706

Browse files
authored
correct issue getHistoricalQuoteData-start-to-give-401-unauthorized-#52 (#54)
Fix issue getHistoricalQuoteData start to give 401 unauthorized #52
1 parent 9dbbcb3 commit 34ec706

9 files changed

Lines changed: 98 additions & 114 deletions

src/ApiClient.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public function getHistoricalQuoteData(string $symbol, string $interval, \DateTi
102102
$this->validateIntervals($interval);
103103
$this->validateDates($startDate, $endDate);
104104

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

107107
return $this->resultDecoder->transformHistoricalDataResult($responseBody);
108108
}
@@ -118,7 +118,7 @@ public function getHistoricalDividendData(string $symbol, \DateTimeInterface $st
118118
{
119119
$this->validateDates($startDate, $endDate);
120120

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

123123
$historicData = $this->resultDecoder->transformDividendDataResult($responseBody);
124124
usort($historicData, function (DividendData $a, DividendData $b): int {
@@ -140,7 +140,7 @@ public function getHistoricalSplitData(string $symbol, \DateTimeInterface $start
140140
{
141141
$this->validateDates($startDate, $endDate);
142142

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

145145
$historicData = $this->resultDecoder->transformSplitDataResult($responseBody);
146146
usort($historicData, function (SplitData $a, SplitData $b): int {
@@ -241,10 +241,10 @@ private function fetchQuotes(array $symbols)
241241
return $this->resultDecoder->transformQuotes($responseBody);
242242
}
243243

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

249249
return (string) $this->client->request('GET', $dataUrl, ['headers' => $this->getHeaders()])->getBody();
250250
}

src/ResultDecoder.php

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -195,81 +195,101 @@ private function validateDate(string $value): \DateTime
195195

196196
public function transformHistoricalDataResult(string $responseBody): array
197197
{
198-
$lines = $this->validateHeaderLines($responseBody, self::HISTORICAL_DATA_HEADER_LINE);
198+
$decoded = json_decode($responseBody, true);
199+
200+
if ((!\is_array($decoded)) || (isset($decoded['chart']['error']))) {
201+
throw new ApiException('Response is not a valid JSON', ApiException::INVALID_RESPONSE);
202+
}
203+
204+
$result = $decoded['chart']['result'][0];
205+
$entryCount = \count($result['timestamp']);
206+
$returnArray = [];
207+
for ($i = 0; $i < $entryCount; ++$i) {
208+
$returnArray[] = $this->createHistoricalData($result, $i);
209+
}
199210

200-
return array_map(function ($line) {
201-
return $this->createHistoricalData(explode(',', $line));
202-
}, $lines);
211+
return $returnArray;
203212
}
204213

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

211-
$date = $this->validateDate($columns[0]);
212-
213-
for ($i = 1; $i <= 6; ++$i) {
214-
if (!is_numeric($columns[$i]) && 'null' !== $columns[$i]) {
215-
throw new ApiException(\sprintf('Not a number in column "%s": %s', self::HISTORICAL_DATA_HEADER_LINE[$i], $columns[$i]), ApiException::INVALID_VALUE);
223+
foreach (['open', 'high', 'low', 'close', 'volume'] as $column) {
224+
$columnValue = $json['indicators']['quote'][0][$column][$index];
225+
if (!is_numeric($columnValue) && 'null' !== $columnValue) {
226+
throw new ApiException(\sprintf('Not a number in column "%s": %s', $column, $column), ApiException::INVALID_VALUE);
216227
}
217228
}
218229

219-
$open = (float) $columns[1];
220-
$high = (float) $columns[2];
221-
$low = (float) $columns[3];
222-
$close = (float) $columns[4];
223-
$adjClose = (float) $columns[5];
224-
$volume = (int) $columns[6];
230+
$columnValue = $json['indicators']['adjclose'][0]['adjclose'][$index];
231+
if (!is_numeric($columnValue) && 'null' !== $columnValue) {
232+
throw new ApiException(\sprintf('Not a number in column "%s": %s', 'adjclose', 'adjclose'), ApiException::INVALID_VALUE);
233+
}
234+
235+
$open = (float) $json['indicators']['quote'][0]['open'][$index];
236+
$high = (float) $json['indicators']['quote'][0]['high'][$index];
237+
$low = (float) $json['indicators']['quote'][0]['low'][$index];
238+
$close = (float) $json['indicators']['quote'][0]['close'][$index];
239+
$volume = (int) $json['indicators']['quote'][0]['volume'][$index];
240+
$adjClose = (float) $json['indicators']['adjclose'][0]['adjclose'][$index];
225241

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

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

233-
return array_map(function ($line) {
234-
return $this->createDividendData(explode(',', $line));
235-
}, $lines);
252+
return array_map(function (array $item) {
253+
return $this->createDividendData($item);
254+
}, $decoded['chart']['result'][0]['events']['dividends']);
236255
}
237256

238-
private function createDividendData(array $columns): DividendData
257+
private function createDividendData(array $json): DividendData
239258
{
240-
if (2 !== \count($columns)) {
241-
throw new ApiException('CSV did not contain correct number of columns', ApiException::INVALID_RESPONSE);
242-
}
243-
244-
$date = $this->validateDate($columns[0]);
245-
246-
if (!is_numeric($columns[1]) && 'null' !== $columns[1]) {
247-
throw new ApiException(\sprintf('Not a number in column Dividends: %s', $columns[1]), ApiException::INVALID_VALUE);
259+
$dateStr = date('Y-m-d', $json['date']);
260+
if ($dateStr) {
261+
$date = $this->validateDate($dateStr);
262+
} else {
263+
throw new ApiException(\sprintf('Not a date in column "Date":%s', $json['date']), ApiException::INVALID_VALUE);
248264
}
249265

250-
$dividends = (float) $columns[1];
266+
$dividends = (float) $json['amount'];
251267

252268
return new DividendData($date, $dividends);
253269
}
254270

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

259-
return array_map(function ($line) {
260-
return $this->createSplitData(explode(',', $line));
261-
}, $lines);
278+
return array_map(function (array $item) {
279+
return $this->createSplitData($item);
280+
}, $decoded['chart']['result'][0]['events']['splits']);
262281
}
263282

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

270-
$date = $this->validateDate($columns[0]);
271-
272-
$stockSplits = (string) $columns[1];
292+
$stockSplits = (string) $json['splitRatio'];
273293

274294
return new SplitData($date, $stockSplits);
275295
}

tests/ResultDecoderTest.php

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,19 @@ public function extractCrumb_invalidStringGiven_throwApiException(): void
106106
*/
107107
public function transformHistoricalDataResult_csvGiven_returnArrayOfHistoricalData(): void
108108
{
109-
$returnedResult = $this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/historicalData.csv'));
109+
$returnedResult = $this->resultDecoder->transformHistoricalDataResult(file_get_contents(__DIR__.'/fixtures/historicalData.json'));
110110

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

114114
$expectedExchangeRate = new HistoricalData(
115-
new \DateTime('2017-07-11'),
116-
144.729996,
117-
145.850006,
118-
144.380005,
119-
145.529999,
120-
145.529999,
121-
19781800
115+
new \DateTime('2024-09-30', new \DateTimeZone('UTC')),
116+
230.0399932861328,
117+
233.0,
118+
229.64999389648438,
119+
233.0,
120+
233.0,
121+
54541900
122122
);
123123
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
124124
}
@@ -129,7 +129,7 @@ public function transformHistoricalDataResult_csvGiven_returnArrayOfHistoricalDa
129129
public function transformHistoricalDataResult_invalidColumnsCsvGiven_throwApiException(): void
130130
{
131131
$this->expectException(ApiException::class);
132-
$this->expectExceptionMessage('CSV did not contain correct number of columns');
132+
$this->expectExceptionMessage('Response is not a valid JSON');
133133

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

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

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

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

178178
$this->assertIsArray($returnedResult);
179179
$this->assertContainsOnlyInstancesOf(DividendData::class, $returnedResult);
180+
$firstResult = array_shift($returnedResult);
180181

181182
$expectedExchangeRate = new DividendData(
182-
new \DateTime('2017-07-11'),
183-
0.205
183+
new \DateTime('2019-11-07', new \DateTimeZone('UTC')),
184+
0.1925
184185
);
185-
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
186+
$this->assertEquals($expectedExchangeRate, $firstResult);
186187
}
187188

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

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

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

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

230231
$this->resultDecoder->transformDividendDataResult(file_get_contents(__DIR__.'/fixtures/invalidNumericStringDividendData.csv'));
231232
}
@@ -235,16 +236,17 @@ public function transformDividendDataResult_invalidNumericStringCsvGiven_throwAp
235236
*/
236237
public function transformSplitDataResult_csvGiven_returnArrayOfSplitData(): void
237238
{
238-
$returnedResult = $this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/splitData.csv'));
239+
$returnedResult = $this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/splitData.json'));
239240

240241
$this->assertIsArray($returnedResult);
241242
$this->assertContainsOnlyInstancesOf(SplitData::class, $returnedResult);
243+
$firstResult = array_shift($returnedResult);
242244

243245
$expectedExchangeRate = new SplitData(
244-
new \DateTime('2017-07-11'),
246+
new \DateTime('2020-08-31', new \DateTimeZone('UTC')),
245247
'4:1'
246248
);
247-
$this->assertEquals($expectedExchangeRate, $returnedResult[0]);
249+
$this->assertEquals($expectedExchangeRate, $firstResult);
248250
}
249251

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

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

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

281283
$this->resultDecoder->transformSplitDataResult(file_get_contents(__DIR__.'/fixtures/invalidDateTimeFormatSplitData.csv'));
282284
}

tests/fixtures/dividendData.csv

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)