Skip to content

Commit f8ace12

Browse files
authored
Merge pull request #9 from adborbas/adborbas/timeseries_daily_adjusted
Add function to handle timeseries daily adjusted endpoint.
2 parents 6d86e5a + 09306f7 commit f8ace12

File tree

11 files changed

+179
-44
lines changed

11 files changed

+179
-44
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let symbols = try await service.symbolSearch(keywords: "VWCE")
2626
- [Symbol Search](https://www.alphavantage.co/documentation/#symbolsearch)
2727
- [Quote](https://www.alphavantage.co/documentation/#latestprice)
2828
- [Exchange Rates](https://www.alphavantage.co/documentation/#currency-exchange)
29+
- [Time Series - Daily Adjusted](https://www.alphavantage.co/documentation/#dailyadj)
2930

3031
## API Key
3132

Sources/AlphaSwiftage/Private/AlphaVantageAPI.swift

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,63 @@ import Foundation
33

44
enum AlphaVantageAPI: URLConvertible {
55
private static let baseURLString = "https://www.alphavantage.co/query"
6-
6+
77
case globalQuote(symbol: String, apiKey: String)
88
case currencyExchangeRate(from: String, to: String, apiKey: String)
99
case symbolSearch(keywords: String, apiKey: String)
10-
11-
func asURL() throws -> URL {
12-
let url = try AlphaVantageAPI.baseURLString.asURL()
13-
14-
switch self {
15-
case .globalQuote(let symbol, let apiKey):
16-
return url.appending("function", value: "GLOBAL_QUOTE")
17-
.appending("symbol", value: symbol)
18-
.appending("apikey", value: apiKey)
19-
case .currencyExchangeRate(let base, let target, let apiKey):
20-
return url.appending("function", value: "CURRENCY_EXCHANGE_RATE")
21-
.appending("from_currency", value: base)
22-
.appending("to_currency", value: target)
23-
.appending("apikey", value: apiKey)
24-
case .symbolSearch(let keywords, let apiKey):
25-
return url.appending("function", value: "SYMBOL_SEARCH")
26-
.appending("keywords", value: keywords)
27-
.appending("apikey", value: apiKey)
28-
}
29-
}
10+
case dailyAdjustedTimeSeries(symbol: String, apiKey: String)
11+
12+
private var functionName: String {
13+
switch self {
14+
case .globalQuote: "GLOBAL_QUOTE"
15+
case .currencyExchangeRate: "CURRENCY_EXCHANGE_RATE"
16+
case .symbolSearch: "SYMBOL_SEARCH"
17+
case .dailyAdjustedTimeSeries: "TIME_SERIES_DAILY_ADJUSTED"
18+
}
19+
}
20+
21+
fileprivate enum Parameter: String {
22+
case symbol
23+
case apiKey = "apikey"
24+
case function
25+
case fromCurrency = "from_currency"
26+
case toCurrency = "to_currency"
27+
case keywords
28+
}
29+
30+
func asURL() throws -> URL {
31+
let url = try AlphaVantageAPI.baseURLString.asURL()
32+
33+
switch self {
34+
case .globalQuote(let symbol, let apiKey):
35+
return url.appendingParameter(.function, value: functionName)
36+
.appendingParameter(.symbol, value: symbol)
37+
.appendingParameter(.apiKey, value: apiKey)
38+
39+
case .currencyExchangeRate(let base, let target, let apiKey):
40+
return url.appendingParameter(.function, value: functionName)
41+
.appendingParameter(.fromCurrency, value: base)
42+
.appendingParameter(.toCurrency, value: target)
43+
.appendingParameter(.apiKey, value: apiKey)
44+
45+
case .symbolSearch(let keywords, let apiKey):
46+
return url.appendingParameter(.function, value: functionName)
47+
.appendingParameter(.keywords, value: keywords)
48+
.appendingParameter(.apiKey, value: apiKey)
49+
50+
case .dailyAdjustedTimeSeries(let symbol, let apiKey):
51+
return url.appendingParameter(.function, value: functionName)
52+
.appendingParameter(.symbol, value: symbol)
53+
.appendingParameter(.apiKey, value: apiKey)
54+
}
55+
}
3056
}
3157

3258
fileprivate extension URL {
33-
func appending(_ queryItem: String, value: String) -> URL {
59+
func appendingParameter(_ parameter: AlphaVantageAPI.Parameter, value: String) -> URL {
3460
guard var urlComponents = URLComponents(string: absoluteString) else { return absoluteURL }
3561
var queryItems: [URLQueryItem] = urlComponents.queryItems ?? []
36-
let queryItem = URLQueryItem(name: queryItem, value: value)
62+
let queryItem = URLQueryItem(name: parameter.rawValue, value: value)
3763
queryItems.append(queryItem)
3864
urlComponents.queryItems = queryItems
3965
return urlComponents.url!

Sources/AlphaSwiftage/Public/AlphaVantageError.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// AlphaVantageError.swift
3-
//
4-
//
5-
// Created by Adam Borbas on 22/03/2024.
6-
//
7-
81
import Foundation
92

103
public enum AlphaVantageError: Error, LocalizedError {

Sources/AlphaSwiftage/Public/AlphaVantageService.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ public class AlphaVantageService {
3333
return await session.request(request)
3434
.serializingAlphaVantageWrappedResponse(SearchSymbolResponse.self) { $0.matches }
3535
}
36+
37+
public func dailyAdjustedTimeSeries(for symbol: String) async -> Result<[String: EquityDailyData], AlphaVantageError> {
38+
let request = AlphaVantageAPI.dailyAdjustedTimeSeries(symbol: symbol, apiKey: apiKey)
39+
return await session.request(request)
40+
.serializingAlphaVantageWrappedResponse(TimeSeriesResponse.self) { $0.dailyTimeSeries }
41+
}
3642
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
3+
public struct EquityDailyData: Codable, Equatable {
4+
public let open: Decimal
5+
public let high: Decimal
6+
public let low: Decimal
7+
public let close: Decimal
8+
public let adjustedClose: Decimal
9+
public let volume: Int
10+
public let dividendAmount: Decimal
11+
public let splitCoefficient: Decimal
12+
13+
enum CodingKeys: String, CodingKey {
14+
case open = "1. open"
15+
case high = "2. high"
16+
case low = "3. low"
17+
case close = "4. close"
18+
case adjustedClose = "5. adjusted close"
19+
case volume = "6. volume"
20+
case dividendAmount = "7. dividend amount"
21+
case splitCoefficient = "8. split coefficient"
22+
}
23+
24+
public init(open: Decimal, high: Decimal, low: Decimal, close: Decimal, adjustedClose: Decimal, volume: Int, dividendAmount: Decimal, splitCoefficient: Decimal) {
25+
self.open = open
26+
self.high = high
27+
self.low = low
28+
self.close = close
29+
self.adjustedClose = adjustedClose
30+
self.volume = volume
31+
self.dividendAmount = dividendAmount
32+
self.splitCoefficient = splitCoefficient
33+
}
34+
35+
public init(from decoder: Decoder) throws {
36+
let container = try decoder.container(keyedBy: CodingKeys.self)
37+
open = try container.decodeUSDecimal(forKey: .open)
38+
high = try container.decodeUSDecimal(forKey: .high)
39+
low = try container.decodeUSDecimal(forKey: .low)
40+
close = try container.decodeUSDecimal(forKey: .close)
41+
adjustedClose = try container.decodeUSDecimal(forKey: .adjustedClose)
42+
volume = try container.decodeUSInt(forKey: .volume)
43+
dividendAmount = try container.decodeUSDecimal(forKey: .dividendAmount)
44+
splitCoefficient = try container.decodeUSDecimal(forKey: .splitCoefficient)
45+
}
46+
}

Sources/AlphaSwiftage/Public/Model/SearchSymbolResponse.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// SearchSymbolResponse.swift
3-
//
4-
//
5-
// Created by Adam Borbas on 16/03/2024.
6-
//
7-
81
import Foundation
92

103
struct SearchSymbolResponse: Codable {

Sources/AlphaSwiftage/Public/Model/Symbol.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// Symbol.swift
3-
//
4-
//
5-
// Created by Adam Borbas on 16/03/2024.
6-
//
7-
81
import Foundation
92

103
public struct Symbol: Codable, Equatable {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Foundation
2+
3+
struct TimeSeriesResponse: Codable {
4+
let metaData: MetaData
5+
let dailyTimeSeries: [String: EquityDailyData]
6+
7+
enum CodingKeys: String, CodingKey {
8+
case metaData = "Meta Data"
9+
case dailyTimeSeries = "Time Series (Daily)"
10+
}
11+
}
12+
13+
struct MetaData: Codable {
14+
let information: String
15+
let symbol: String
16+
let lastRefreshed: String
17+
let outputSize: String
18+
let timeZone: String
19+
20+
enum CodingKeys: String, CodingKey {
21+
case information = "1. Information"
22+
case symbol = "2. Symbol"
23+
case lastRefreshed = "3. Last Refreshed"
24+
case outputSize = "4. Output Size"
25+
case timeZone = "5. Time Zone"
26+
}
27+
}

Tests/AlphaVantageServiceTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ final class AlphaVantageServiceTests: XCTestCase {
8989
await assertError(result, expectedError: expectedError)
9090
}
9191

92+
func testDailyTimeSeries() async {
93+
// Given
94+
let expectedResult: [String: EquityDailyData] = [
95+
"2024-06-21" : EquityDailyData(open: 173.97, high: 174.96, low: 171.4, close: 172.46, adjustedClose: 172.46, volume: 10182025, dividendAmount: 0, splitCoefficient: 1.0),
96+
"2024-06-20": EquityDailyData(open: 174.08, high: 174.28, low: 171.22, close: 173.92, adjustedClose: 173.92, volume: 4723078, dividendAmount: 0, splitCoefficient: 1.0)
97+
]
98+
99+
let symbol = "MSFT"
100+
let service = givenService()
101+
given(response: .dailyTimeSeriesSuccess, for: "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=\(symbol)&apikey=\(apiKey)")
102+
103+
// When
104+
let result = await service.dailyAdjustedTimeSeries(for: symbol)
105+
106+
// Then
107+
assertSuccess(result, expectedValue: expectedResult)
108+
}
109+
92110
func test_unexpectedResponse() async {
93111
// Given
94112
let symbol = "vwce"

Tests/MockResponse.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum MockResponse: String {
1313
case currencyExchangeRate = "mockCurrencyExchangeRateeResponse"
1414
case symbolSearchSuccess = "mockSymbolSearchSuccess"
1515
case symbolSearchFailure = "mockSymbolSearchFailure"
16+
case dailyTimeSeriesSuccess = "mockDailyTimeSeriesSuccess"
1617
case unexpectedResponse = "mockUnexpectedResponse"
1718

1819
var resourcePath: URL {

0 commit comments

Comments
 (0)