Fix YahooFinanceClient Swift 6 actor isolation and type-path errors
Two bugs: 1. Swift 6 strict concurrency: JSONDecoder().decode() inside actor-isolated methods triggers "Main actor-isolated conformance cannot be used in actor-isolated context". Fix: move all Decodable usage into nonisolated helper methods (parseChart, parseOptions, makeContract) so the conformance is invoked outside the actor's isolation boundary. 2. Wrong nested type path: OptionsResponse.OptionData.YFOption does not exist; YFOption is nested directly under OptionsResponse. Fix: reference OptionsResponse.YFOption everywhere and remove the old private extension in favour of the nonisolated makeContract(from:) factory method. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,17 +13,17 @@ struct PriceBar {
|
|||||||
struct PriceHistory {
|
struct PriceHistory {
|
||||||
let ticker: String
|
let ticker: String
|
||||||
let bars: [PriceBar]
|
let bars: [PriceBar]
|
||||||
var closes: [Double] { bars.map(\.close) }
|
var closes: [Double] { bars.map(\.close) }
|
||||||
var highs: [Double] { bars.map(\.high) }
|
var highs: [Double] { bars.map(\.high) }
|
||||||
var lows: [Double] { bars.map(\.low) }
|
var lows: [Double] { bars.map(\.low) }
|
||||||
var currentPrice: Double { bars.last?.close ?? 0 }
|
var currentPrice: Double { bars.last?.close ?? 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OptionContract {
|
struct OptionContract {
|
||||||
let strike: Double
|
let strike: Double
|
||||||
let bid: Double
|
let bid: Double
|
||||||
let ask: Double
|
let ask: Double
|
||||||
let impliedVolatility: Double // annualised fraction (e.g. 0.30 = 30%)
|
let impliedVolatility: Double // annualised fraction (e.g. 0.30 = 30 %)
|
||||||
let volume: Int
|
let volume: Int
|
||||||
let openInterest: Int
|
let openInterest: Int
|
||||||
var mid: Double { (bid + ask) / 2 }
|
var mid: Double { (bid + ask) / 2 }
|
||||||
@@ -40,8 +40,8 @@ enum YFError: LocalizedError {
|
|||||||
case badURL
|
case badURL
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .noData: return "No data returned from Yahoo Finance."
|
case .noData: return "No data returned from Yahoo Finance."
|
||||||
case .badURL: return "Could not construct Yahoo Finance URL."
|
case .badURL: return "Could not construct Yahoo Finance URL."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ actor YahooFinanceClient {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
private var cache: [String: (value: Any, storedAt: Date)] = [:]
|
private var cache: [String: (value: Any, storedAt: Date)] = [:]
|
||||||
private let ttl: TimeInterval = 900 // 15 min
|
private let ttl: TimeInterval = 900 // 15 min
|
||||||
|
|
||||||
private func get<T>(_ key: String) -> T? {
|
private func get<T>(_ key: String) -> T? {
|
||||||
guard let entry = cache[key],
|
guard let entry = cache[key],
|
||||||
@@ -72,12 +72,22 @@ actor YahooFinanceClient {
|
|||||||
let typed = entry.value as? T else { return nil }
|
let typed = entry.value as? T else { return nil }
|
||||||
return typed
|
return typed
|
||||||
}
|
}
|
||||||
private func set(_ value: Any, key: String) {
|
private func set(_ value: Any, key: String) { cache[key] = (value, Date()) }
|
||||||
cache[key] = (value, Date())
|
|
||||||
}
|
|
||||||
func clearCache() { cache.removeAll() }
|
func clearCache() { cache.removeAll() }
|
||||||
|
|
||||||
// MARK: Price history
|
// MARK: - nonisolated decode helpers
|
||||||
|
// Placed outside actor isolation so Swift 6 doesn't flag the Decodable
|
||||||
|
// conformance as being used in an actor-isolated context.
|
||||||
|
|
||||||
|
nonisolated private func parseChart(_ data: Data) throws -> ChartResponse {
|
||||||
|
try JSONDecoder().decode(ChartResponse.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated private func parseOptions(_ data: Data) throws -> OptionsResponse {
|
||||||
|
try JSONDecoder().decode(OptionsResponse.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Price history
|
||||||
|
|
||||||
func priceHistory(ticker: String) async throws -> PriceHistory {
|
func priceHistory(ticker: String) async throws -> PriceHistory {
|
||||||
let key = "hist_\(ticker)"
|
let key = "hist_\(ticker)"
|
||||||
@@ -88,13 +98,13 @@ actor YahooFinanceClient {
|
|||||||
else { throw YFError.badURL }
|
else { throw YFError.badURL }
|
||||||
|
|
||||||
let (data, _) = try await session.data(from: url)
|
let (data, _) = try await session.data(from: url)
|
||||||
let resp = try JSONDecoder().decode(ChartResponse.self, from: data)
|
let resp = try parseChart(data)
|
||||||
|
|
||||||
guard let r = resp.chart.result?.first,
|
guard let r = resp.chart.result?.first,
|
||||||
let q = r.indicators.quote.first else { throw YFError.noData }
|
let q = r.indicators.quote.first else { throw YFError.noData }
|
||||||
|
|
||||||
var bars: [PriceBar] = []
|
var bars: [PriceBar] = []
|
||||||
let ts = r.timestamp
|
let ts = r.timestamp
|
||||||
let opens = q.open
|
let opens = q.open
|
||||||
let highs = q.high
|
let highs = q.high
|
||||||
let lows = q.low
|
let lows = q.low
|
||||||
@@ -116,7 +126,7 @@ actor YahooFinanceClient {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Option expirations
|
// MARK: - Option expirations
|
||||||
|
|
||||||
func expirations(ticker: String) async throws -> [Date] {
|
func expirations(ticker: String) async throws -> [Date] {
|
||||||
let key = "exp_\(ticker)"
|
let key = "exp_\(ticker)"
|
||||||
@@ -127,7 +137,7 @@ actor YahooFinanceClient {
|
|||||||
else { throw YFError.badURL }
|
else { throw YFError.badURL }
|
||||||
|
|
||||||
let (data, _) = try await session.data(from: url)
|
let (data, _) = try await session.data(from: url)
|
||||||
let resp = try JSONDecoder().decode(OptionsResponse.self, from: data)
|
let resp = try parseOptions(data)
|
||||||
|
|
||||||
guard let r = resp.optionChain.result?.first else { throw YFError.noData }
|
guard let r = resp.optionChain.result?.first else { throw YFError.noData }
|
||||||
let dates = r.expirationDates.map { Date(timeIntervalSince1970: Double($0)) }
|
let dates = r.expirationDates.map { Date(timeIntervalSince1970: Double($0)) }
|
||||||
@@ -148,7 +158,7 @@ actor YahooFinanceClient {
|
|||||||
return exps.first { Calendar.current.isDate($0, inSameDayAs: today) }
|
return exps.first { Calendar.current.isDate($0, inSameDayAs: today) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Option chain
|
// MARK: - Option chain
|
||||||
|
|
||||||
func optionChain(ticker: String, expiration: Date) async throws -> OptionChain {
|
func optionChain(ticker: String, expiration: Date) async throws -> OptionChain {
|
||||||
let epoch = Int(expiration.timeIntervalSince1970)
|
let epoch = Int(expiration.timeIntervalSince1970)
|
||||||
@@ -160,20 +170,34 @@ actor YahooFinanceClient {
|
|||||||
else { throw YFError.badURL }
|
else { throw YFError.badURL }
|
||||||
|
|
||||||
let (data, _) = try await session.data(from: url)
|
let (data, _) = try await session.data(from: url)
|
||||||
let resp = try JSONDecoder().decode(OptionsResponse.self, from: data)
|
let resp = try parseOptions(data)
|
||||||
|
|
||||||
guard let r = resp.optionChain.result?.first,
|
guard let r = resp.optionChain.result?.first,
|
||||||
let opts = r.options.first else { throw YFError.noData }
|
let opts = r.options.first else { throw YFError.noData }
|
||||||
|
|
||||||
let calls = opts.calls.compactMap { OptionContract(from: $0) }
|
let calls = opts.calls.compactMap { makeContract(from: $0) }
|
||||||
let puts = opts.puts .compactMap { OptionContract(from: $0) }
|
let puts = opts.puts .compactMap { makeContract(from: $0) }
|
||||||
|
|
||||||
let chain = OptionChain(expiration: expiration, calls: calls, puts: puts)
|
let chain = OptionChain(expiration: expiration, calls: calls, puts: puts)
|
||||||
set(chain, key: key)
|
set(chain, key: key)
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Earnings date
|
/// nonisolated factory so the OptionsResponse.YFOption type is not
|
||||||
|
/// referenced inside actor-isolated code (avoids Swift 6 conformance error).
|
||||||
|
nonisolated private func makeContract(from yf: OptionsResponse.YFOption) -> OptionContract? {
|
||||||
|
guard let bid = yf.bid, let ask = yf.ask, bid >= 0, ask >= 0 else { return nil }
|
||||||
|
return OptionContract(
|
||||||
|
strike: yf.strike,
|
||||||
|
bid: bid,
|
||||||
|
ask: ask,
|
||||||
|
impliedVolatility: yf.impliedVolatility ?? 0.3,
|
||||||
|
volume: yf.volume ?? 0,
|
||||||
|
openInterest: yf.openInterest ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Earnings date
|
||||||
|
|
||||||
func nextEarningsDate(ticker: String) async throws -> Date? {
|
func nextEarningsDate(ticker: String) async throws -> Date? {
|
||||||
let key = "earn_\(ticker)"
|
let key = "earn_\(ticker)"
|
||||||
@@ -184,32 +208,32 @@ actor YahooFinanceClient {
|
|||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
guard let (data, _) = try? await session.data(from: url),
|
guard let (data, _) = try? await session.data(from: url),
|
||||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
let qs = json["quoteSummary"] as? [String: Any],
|
let qs = json["quoteSummary"] as? [String: Any],
|
||||||
let results = qs["result"] as? [[String: Any]],
|
let results = qs["result"] as? [[String: Any]],
|
||||||
let first = results.first,
|
let first = results.first,
|
||||||
let cal = first["calendarEvents"] as? [String: Any],
|
let cal = first["calendarEvents"] as? [String: Any],
|
||||||
let earn = cal["earnings"] as? [String: Any],
|
let earn = cal["earnings"] as? [String: Any],
|
||||||
let dates = earn["earningsDate"] as? [[String: Any]],
|
let dates = earn["earningsDate"] as? [[String: Any]],
|
||||||
let rawDate = dates.first?["raw"] as? TimeInterval
|
let rawDate = dates.first?["raw"] as? TimeInterval
|
||||||
else {
|
else {
|
||||||
set(Optional<Date>.none as Any, key: key)
|
set(Optional<Date>.none as Any, key: key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let date: Date? = Date(timeIntervalSince1970: rawDate) > Date()
|
let date: Date? = Date(timeIntervalSince1970: rawDate) > Date()
|
||||||
? Date(timeIntervalSince1970: rawDate)
|
? Date(timeIntervalSince1970: rawDate) : nil
|
||||||
: nil
|
|
||||||
set(date as Any, key: key)
|
set(date as Any, key: key)
|
||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private JSON models
|
// MARK: - Private JSON models
|
||||||
|
// Defined at file scope (outside the actor) as plain Decodable value types.
|
||||||
|
|
||||||
private struct ChartResponse: Decodable {
|
private struct ChartResponse: Decodable {
|
||||||
let chart: Wrapper
|
let chart: Wrapper
|
||||||
struct Wrapper: Decodable { let result: [Result]? }
|
struct Wrapper: Decodable { let result: [Result]? }
|
||||||
struct Result: Decodable {
|
struct Result: Decodable {
|
||||||
let timestamp: [Int]
|
let timestamp: [Int]
|
||||||
let indicators: Indicators
|
let indicators: Indicators
|
||||||
@@ -225,7 +249,7 @@ private struct ChartResponse: Decodable {
|
|||||||
|
|
||||||
private struct OptionsResponse: Decodable {
|
private struct OptionsResponse: Decodable {
|
||||||
let optionChain: Wrapper
|
let optionChain: Wrapper
|
||||||
struct Wrapper: Decodable { let result: [Result]? }
|
struct Wrapper: Decodable { let result: [Result]? }
|
||||||
struct Result: Decodable {
|
struct Result: Decodable {
|
||||||
let expirationDates: [Int]
|
let expirationDates: [Int]
|
||||||
let options: [OptionData]
|
let options: [OptionData]
|
||||||
@@ -234,6 +258,7 @@ private struct OptionsResponse: Decodable {
|
|||||||
let calls: [YFOption]
|
let calls: [YFOption]
|
||||||
let puts: [YFOption]
|
let puts: [YFOption]
|
||||||
}
|
}
|
||||||
|
// YFOption is a direct nested type of OptionsResponse, NOT of OptionData.
|
||||||
struct YFOption: Decodable {
|
struct YFOption: Decodable {
|
||||||
let strike: Double
|
let strike: Double
|
||||||
let bid: Double?
|
let bid: Double?
|
||||||
@@ -243,15 +268,3 @@ private struct OptionsResponse: Decodable {
|
|||||||
let openInterest: Int?
|
let openInterest: Int?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension OptionContract {
|
|
||||||
init?(from yf: OptionsResponse.OptionData.YFOption) {
|
|
||||||
guard let bid = yf.bid, let ask = yf.ask, bid >= 0, ask >= 0 else { return nil }
|
|
||||||
self.strike = yf.strike
|
|
||||||
self.bid = bid
|
|
||||||
self.ask = ask
|
|
||||||
self.impliedVolatility = yf.impliedVolatility ?? 0.3
|
|
||||||
self.volume = yf.volume ?? 0
|
|
||||||
self.openInterest = yf.openInterest ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user