b2970e4c-45a2-423e-b84d-257.../Sources/data/GoldClient.swift
徐翔宇 e5a72bcf42 v0.2.0: initial 看盘侠 — Fund & Gold plugin for MioIsland
Real-time OTC fund and gold tracker for the macOS notch. Three tabs:
- 持仓: live intraday estimates from 天天基金, hero card with
  total value / today P&L / cumulative P&L, per-row 当日 + 累计.
- 黄金: SHFE 沪金 realtime + daily K-line + 伦敦金 reference.
- 添加: search 26k+ public funds via 东方财富 suggest API.

No API keys, no Python, no servers. All data comes from public
endpoints (天天基金, 东方财富, 新浪财经).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:20:15 +08:00

60 lines
2.0 KiB
Swift

//
// GoldClient.swift
// plugin
//
// Realtime gold quotes from Sina Finance. One endpoint, four flavors.
//
// Sina serves comma-separated field strings, NOT JSON. Encoding is
// GB18030 for Chinese name fields, ASCII for numbers see
// SinaQuoteParser for details.
//
import Foundation
actor GoldClient {
static let shared = GoldClient()
private let session: URLSession
init() {
let cfg = URLSessionConfiguration.default
cfg.timeoutIntervalForRequest = 6
cfg.timeoutIntervalForResource = 12
cfg.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: cfg)
}
// MARK: One source
func quote(for source: GoldQuote.Source) async throws -> GoldQuote {
let symbol = source.sinaSymbol
let url = URL(string: "https://hq.sinajs.cn/list=\(symbol)")!
var req = URLRequest(url: url)
req.setValue("Mozilla/5.0", forHTTPHeaderField: "User-Agent")
// Sina enforces a Referer check on hq.sinajs.cn endpoints
// requests without a finance.sina.com.cn referer return empty
// payloads. Documented hack but it has held since 2016.
req.setValue("https://finance.sina.com.cn/", forHTTPHeaderField: "Referer")
let (data, _) = try await session.data(for: req)
guard let q = SinaQuoteParser.parse(data: data, source: source) else {
throw FundClientError.malformed("sina parse failed for \(symbol)")
}
return q
}
// MARK: Bulk
/// Fetch all four sources in parallel. Failures degrade silently
/// so off-hours empty payloads don't sink the panel.
func quoteAll() async -> [GoldQuote] {
await withTaskGroup(of: GoldQuote?.self) { group in
for src in [GoldQuote.Source.comex, .london, .shfeFutures] {
group.addTask { try? await self.quote(for: src) }
}
var out: [GoldQuote] = []
for await q in group { if let q { out.append(q) } }
return out
}
}
}