b2970e4c-45a2-423e-b84d-257.../Sources/data/SpotGoldClient.swift

103 lines
3.9 KiB
Swift
Raw Normal View History

//
// SpotGoldClient.swift
// plugin v0.3
//
// AU9999 (SGE) realtime same data Alipay shows on the
// tab. Distinct from the SHFE futures AU0 we already pull:
// AU9999 is the SGE physical-spot reference price retail jewellery
// banks quote off, whereas AU0 is the futures contract.
//
// Endpoint: `https://hq.sinajs.cn/list=gds_AU9999`
//
// Field layout (verified 2026-04-26):
// [0] current price 1040.00
// [1] ? 0
// [2] ? 1039.00 (looks like avg or bid?)
// [3] ? 1049.00
// [4] high 1044.00
// [5] low 1034.00
// [6] time "02:30:00"
// [7] prevClose 1033.25
// [8] open 1039.90
// [9] volume 3136
// [10] (purity?) 100.00
// [11] (purity?) 100.00
// [12] date "2026-04-25"
// [13] name (GBK) "99"
//
import Foundation
/// Spot gold quote (SGE AU9999 / AU99.99).
struct SpotGoldQuote: Equatable {
let last: Double // current
let prevClose: Double //
let open: Double //
let high: Double //
let low: Double //
let updatedAt: Date?
let name: String // "99"
var change: Double { last - prevClose }
var changeRate: Double { prevClose == 0 ? 0 : change / prevClose * 100 }
}
actor SpotGoldClient {
static let shared = SpotGoldClient()
private let session: URLSession
init() {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 6
cfg.timeoutIntervalForResource = 12
cfg.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: cfg)
}
/// Fetch the current AU9999 spot quote.
func quote() async throws -> SpotGoldQuote {
let url = URL(string: "https://hq.sinajs.cn/list=gds_AU9999")!
var req = URLRequest(url: url)
req.setValue("Mozilla/5.0", forHTTPHeaderField: "User-Agent")
req.setValue("https://finance.sina.com.cn/", forHTTPHeaderField: "Referer")
let (data, _) = try await session.data(for: req)
// Sina serves GB18030 here too (name field is Chinese).
let gbEnc = String.Encoding(rawValue:
CFStringConvertEncodingToNSStringEncoding(
CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)))
let s = String(data: data, encoding: gbEnc)
?? String(data: data, encoding: .utf8)
?? ""
guard let lQ = s.firstIndex(of: "\""),
let rQ = s[s.index(after: lQ)...].firstIndex(of: "\"") else {
throw FundClientError.malformed("AU9999 payload parse")
}
let body = String(s[s.index(after: lQ)..<rQ])
let fields = body.components(separatedBy: ",")
// Off-hours can return an empty payload; guard.
guard fields.count >= 13,
let last = Double(fields[0]),
let high = Double(fields[4]),
let low = Double(fields[5]),
let prevClose = Double(fields[7]),
let open = Double(fields[8]) else {
throw FundClientError.malformed("AU9999 field decode (\(fields.count) fields)")
}
let timeStr = fields[6]
let dateStr = fields[12]
let fmt = DateFormatter()
fmt.locale = Locale(identifier: "en_US_POSIX")
fmt.timeZone = TimeZone(identifier: "Asia/Shanghai")
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
let updatedAt = fmt.date(from: "\(dateStr) \(timeStr)")
let name = fields.count > 13 ? fields[13] : "上海金"
return SpotGoldQuote(
last: last, prevClose: prevClose, open: open,
high: high, low: low, updatedAt: updatedAt, name: name
)
}
}