mirror of
https://github.com/carey314/mio-plugin-fund.git
synced 2026-08-10 05:44:31 +00:00
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>
60 lines
2.0 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|