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>
74 lines
2.8 KiB
Swift
74 lines
2.8 KiB
Swift
//
|
|
// GoldKlineClient.swift
|
|
// 盯基金 plugin v0.2
|
|
//
|
|
// Daily K-line history for the gold chart. Uses Sina's classic
|
|
// inner-futures endpoint:
|
|
//
|
|
// https://stock2.finance.sina.com.cn/futures/api/json.php/IndexService.getInnerFuturesDailyKLine?symbol=AU0
|
|
//
|
|
// Returns a JSON array of arrays:
|
|
// [[date, open, high, low, close, volume], ...]
|
|
//
|
|
// Stable since 2008, used by every futures-tracker website in China.
|
|
// Falls back gracefully if the endpoint format ever changes — we
|
|
// parse defensively.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
actor GoldKlineClient {
|
|
static let shared = GoldKlineClient()
|
|
private let session: URLSession
|
|
|
|
init() {
|
|
let cfg = URLSessionConfiguration.default
|
|
cfg.timeoutIntervalForRequest = 8
|
|
cfg.timeoutIntervalForResource = 15
|
|
cfg.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
self.session = URLSession(configuration: cfg)
|
|
}
|
|
|
|
/// Fetch the full daily K-line for the given symbol (default AU0
|
|
/// = SHFE 沪金主连). Returns oldest → newest.
|
|
func dailyKLine(symbol: String = "AU0") async throws -> [GoldDailyBar] {
|
|
let url = URL(string:
|
|
"https://stock2.finance.sina.com.cn/futures/api/json.php/IndexService.getInnerFuturesDailyKLine?symbol=\(symbol)"
|
|
)!
|
|
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 returns: [["2008-01-09","230.950","230.990","221.880","223.300","103364"], ...]
|
|
// Strings only — we parse to Double.
|
|
guard let arr = try? JSONSerialization.jsonObject(with: data) as? [[String]] else {
|
|
throw FundClientError.malformed("kline array decode failed")
|
|
}
|
|
|
|
let fmt = DateFormatter()
|
|
fmt.locale = Locale(identifier: "en_US_POSIX")
|
|
fmt.timeZone = TimeZone(identifier: "Asia/Shanghai")
|
|
fmt.dateFormat = "yyyy-MM-dd"
|
|
|
|
return arr.compactMap { row -> GoldDailyBar? in
|
|
guard row.count >= 5,
|
|
let date = fmt.date(from: row[0]),
|
|
let open = Double(row[1]),
|
|
let high = Double(row[2]),
|
|
let low = Double(row[3]),
|
|
let close = Double(row[4]) else { return nil }
|
|
let volume = row.count >= 6 ? (Double(row[5]) ?? 0) : 0
|
|
return GoldDailyBar(date: date, open: open, high: high, low: low, close: close, volume: volume)
|
|
}
|
|
}
|
|
|
|
/// Convenience: slice the most recent N days.
|
|
func recentDailyKLine(days: Int?, symbol: String = "AU0") async throws -> [GoldDailyBar] {
|
|
let full = try await dailyKLine(symbol: symbol)
|
|
guard let n = days, n < full.count else { return full }
|
|
return Array(full.suffix(n))
|
|
}
|
|
}
|