Skip to content

Commit 25308a9

Browse files
author
Wing Chau
committed
Merge branch 'feature/fetch-stocks' into develop
2 parents 23a38f7 + 39eb623 commit 25308a9

28 files changed

+1112
-66
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
.DS_Store
22
/.build
3+
/.swiftpm
34
/Packages
45
/*.xcodeproj
56
xcuserdata/
7+
8+
/Tests/AlphaVantageTests/PrivateConst.swift

Package.resolved

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,36 @@
44
import PackageDescription
55

66
let package = Package(
7-
name: "AlphaVantage",
7+
name: "AlphaVantageExecutable",
8+
products: [
9+
.executable(name: "AlphaVantageExecutable",
10+
targets: ["AlphaVantageExecutable"]),
11+
.library(name: "AlphaVantage", targets: ["AlphaVantage"])
12+
],
813
dependencies: [
914
// Dependencies declare other packages that this package depends on.
1015
// .package(url: /* package url */, from: "1.0.0"),
1116
.package(
1217
url: "https://github.com/IBM-Swift/Configuration.git",
13-
from: "3.0.4")
18+
from: "3.0.4"
19+
),
20+
.package(url: "https://github.com/IBM-Swift/SwiftyRequest",
21+
from: "3.0.0"),
1422
],
1523
targets: [
1624
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
1725
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
26+
.target(
27+
name: "AlphaVantageExecutable",
28+
dependencies: ["AlphaVantage", "Configuration"]
29+
),
1830
.target(
1931
name: "AlphaVantage",
20-
dependencies: ["Configuration"]),
32+
dependencies: ["SwiftyRequest"]
33+
),
2134
.testTarget(
2235
name: "AlphaVantageTests",
23-
dependencies: ["AlphaVantage"]),
36+
dependencies: ["AlphaVantage", "SwiftyRequest"]
37+
),
2438
]
25-
)
39+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
3+
/// Protocol of Alpha Vantage API implementation classes.
4+
public protocol AlphaVantage {
5+
/// API key provided by Alpha Vantage.
6+
var apiKey: String { get }
7+
/// Optional. Data retrieved will be exported to file if set.
8+
var export: (path: URL, dataType: ApiConst.DataType)? { get }
9+
10+
/**
11+
Initialiser.
12+
13+
- parameters:
14+
- apiKey: API key provided by Alpha Vantage.
15+
- export: Declares to save the retrieved data as file in specified format & location or not.
16+
*/
17+
init(apiKey: String, export: (path: URL, dataType: ApiConst.DataType)?)
18+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import Foundation
2+
import SwiftyRequest
3+
4+
private typealias Res = ApiResponse.StockTimeSeries
5+
private typealias ErrRes = ApiResponse.ApiError
6+
7+
/// Implementation of APIs under `Stock Time Series` on Alpha Vantage.
8+
public class Stock: AlphaVantage {
9+
public var apiKey: String
10+
public var export: (path: URL, dataType: ApiConst.DataType)?
11+
12+
public required init(
13+
apiKey: String,
14+
export: (path: URL, dataType: ApiConst.DataType)? = nil
15+
) {
16+
self.apiKey = apiKey
17+
self.export = export
18+
}
19+
20+
/**
21+
Request stock intraday market data from API.
22+
23+
- parameters:
24+
- symbol: The symbol of target equity.
25+
- interval: Time interval between two consecutive data points in the
26+
time series.
27+
- completion: A closure to be executed once the request has finished.
28+
*/
29+
public func fetchStockIntraday(
30+
symbol: String,
31+
interval: ApiConst.Stock.IntradayInterval,
32+
completion: @escaping (
33+
_ result: ApiResponse.StockTimeSeries.STSIntraday?,
34+
_ err: Error?
35+
) -> Void
36+
) {
37+
let request = RestRequest(
38+
method: .get,
39+
url: apiUrl(function: .intraday(interval: interval), symbol: symbol)
40+
)
41+
// Uses `.responseData` instead of `.responseObject` to avoid
42+
// implementing another response block to retrieve binary `Data`
43+
// for exporting JSON/CSV file.
44+
request.responseData { result in
45+
switch result {
46+
case let .success(res):
47+
guard let decoded = try? JSONDecoder().decode(
48+
Res.STSIntraday.self, from: res.body
49+
) else {
50+
if let errRes = try? JSONDecoder().decode(
51+
ErrRes.self, from: res.body
52+
) {
53+
completion(nil, errRes)
54+
} else {
55+
completion(nil, ErrRes(errMsg: "Unknown Error"))
56+
}
57+
58+
return
59+
}
60+
61+
var err: Error?
62+
63+
if let export = self.export {
64+
let filename = "intraday_\(interval.rawValue)_\(symbol)"
65+
do {
66+
try self.handleExport(data: res.body,
67+
filename: filename,
68+
export)
69+
} catch {
70+
err = error
71+
}
72+
}
73+
74+
completion(decoded, err)
75+
case let .failure(err):
76+
completion(nil, err)
77+
} // - end switch
78+
} // - end completion
79+
} // - end fetchStockIntraday
80+
81+
/**
82+
Request stock daily market data from API
83+
84+
- Parameters:
85+
- symbol: The symbol of target equity.
86+
- completion: A closure to be executed once the request has finished.
87+
*/
88+
public func fetchStockDaily(
89+
symbol: String,
90+
completion: @escaping (
91+
_ result: ApiResponse.StockTimeSeries.STSDaily?,
92+
_ err: Error?
93+
) -> Void
94+
) {
95+
let request = RestRequest(
96+
method: .get, url: apiUrl(function: .daily, symbol: symbol)
97+
)
98+
request.responseData { result in
99+
switch result {
100+
case let .success(res):
101+
guard let decoded = try? JSONDecoder().decode(
102+
Res.STSDaily.self, from: res.body
103+
) else {
104+
if let errRes = try? JSONDecoder().decode(
105+
ErrRes.self, from: res.body
106+
) {
107+
completion(nil, errRes)
108+
} else {
109+
completion(nil, ErrRes(errMsg: "Unknown Error"))
110+
}
111+
112+
return
113+
}
114+
115+
var err: Error?
116+
117+
if let export = self.export {
118+
let filename = "daily_\(symbol)"
119+
do {
120+
try self.handleExport(data: res.body,
121+
filename: filename,
122+
export)
123+
} catch {
124+
err = error
125+
}
126+
}
127+
128+
completion(decoded, err)
129+
case let .failure(err):
130+
completion(nil, err)
131+
} // - end switch
132+
} // - end completion
133+
}
134+
135+
/**
136+
Request stock adjusted daily market data from API
137+
138+
- Parameters:
139+
- symbol: The symbol of target equity.
140+
- completion: A closure to be executed once the request has finished.
141+
*/
142+
public func fetchStockDailyAdjusted(
143+
symbol: String,
144+
completion: @escaping (
145+
_ result: ApiResponse.StockTimeSeries.STSDailyAdjusted?,
146+
_ err: Error?
147+
) -> Void
148+
) {
149+
let request = RestRequest(
150+
method: .get, url: apiUrl(function: .dailyAdjusted, symbol: symbol)
151+
)
152+
request.responseData { result in
153+
switch result {
154+
case let .success(res):
155+
guard let decoded = try? JSONDecoder().decode(
156+
Res.STSDailyAdjusted.self, from: res.body
157+
) else {
158+
if let errRes = try? JSONDecoder().decode(
159+
ErrRes.self, from: res.body
160+
) {
161+
completion(nil, errRes)
162+
} else {
163+
completion(nil, ErrRes(errMsg: "Unknown Error"))
164+
}
165+
166+
return
167+
}
168+
169+
var err: Error?
170+
171+
if let export = self.export {
172+
let filename = "daily_adjusted_\(symbol)"
173+
do {
174+
try self.handleExport(data: res.body,
175+
filename: filename,
176+
export)
177+
} catch {
178+
err = error
179+
}
180+
}
181+
182+
completion(decoded, err)
183+
case let .failure(err):
184+
completion(nil, err)
185+
} // - end switch
186+
} // - end completion
187+
}
188+
189+
private func apiUrl(function: ApiConst.Stock.Function,
190+
symbol: String) -> String
191+
{
192+
return ApiConst.Stock.api(
193+
function: function,
194+
symbol: symbol,
195+
dataType: export?.dataType ?? .json,
196+
apiKey: apiKey
197+
)
198+
}
199+
200+
private func handleExport(
201+
data: Data,
202+
filename: String,
203+
_ meta: (path: URL, dataType: ApiConst.DataType)
204+
) throws {
205+
let fullPath = meta.path.appendingPathComponent(
206+
"/\(filename).\(meta.dataType.rawValue)"
207+
)
208+
209+
try DataExporter.export(data, to: fullPath)
210+
}
211+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
/// Utility to write binary data to file & save in specified location.
4+
struct DataExporter {
5+
private init() {}
6+
7+
/// Export `Data` to file in specified location.
8+
/// - Parameters:
9+
/// - data: Binary data to be written to target location.
10+
/// - path: Full path of the targeted location, including file name & file extension.
11+
/// e.g. `/root/file.json
12+
static func export(_ data: Data, to path: URL) throws {
13+
try data.write(to: path, options: .atomic)
14+
}
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// `CodingKey` struct for dynamic keys.
2+
struct GenericCodingKeys: CodingKey {
3+
var intValue: Int?
4+
var stringValue: String
5+
6+
init?(intValue: Int) {
7+
self.intValue = intValue
8+
stringValue = "\(intValue)"
9+
}
10+
11+
init?(stringValue: String) {
12+
self.stringValue = stringValue
13+
}
14+
}

0 commit comments

Comments
 (0)