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>
119 lines
3.5 KiB
Swift
119 lines
3.5 KiB
Swift
//
|
|
// Watchlist.swift
|
|
// 盯基金 plugin v0.2
|
|
//
|
|
// User's selected funds + their cost basis / shares (when set).
|
|
// Persisted as JSON to:
|
|
// ~/Library/Application Support/Mio Island/Fund/watchlist.json
|
|
//
|
|
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class Watchlist: ObservableObject {
|
|
@Published private(set) var funds: [WatchlistFund] = []
|
|
|
|
private let storeURL: URL
|
|
private let queue = DispatchQueue(label: "com.mioisland.plugin.fund.watchlist", qos: .userInitiated)
|
|
|
|
init() {
|
|
let appSupport = FileManager.default
|
|
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
let dir = appSupport.appendingPathComponent("Mio Island/Fund", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
self.storeURL = dir.appendingPathComponent("watchlist.json")
|
|
load()
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
func add(_ hit: FundSearchHit) {
|
|
guard !funds.contains(where: { $0.code == hit.code }) else { return }
|
|
let item = WatchlistFund(
|
|
code: hit.code, name: hit.name,
|
|
addedAt: Date(),
|
|
displayOrder: (funds.map { $0.displayOrder }.max() ?? 0) + 1,
|
|
shares: nil, costNav: nil
|
|
)
|
|
funds.append(item)
|
|
save()
|
|
}
|
|
|
|
func remove(code: String) {
|
|
funds.removeAll { $0.code == code }
|
|
save()
|
|
}
|
|
|
|
func updatePosition(code: String, shares: Double?, costNav: Double?) {
|
|
guard let idx = funds.firstIndex(where: { $0.code == code }) else { return }
|
|
var f = funds[idx]
|
|
f.shares = shares
|
|
f.costNav = costNav
|
|
funds[idx] = f
|
|
save()
|
|
}
|
|
|
|
func move(from source: IndexSet, to destination: Int) {
|
|
funds.move(fromOffsets: source, toOffset: destination)
|
|
for (idx, var f) in funds.enumerated() {
|
|
f.displayOrder = idx
|
|
funds[idx] = f
|
|
}
|
|
save()
|
|
}
|
|
|
|
func contains(_ code: String) -> Bool {
|
|
funds.contains { $0.code == code }
|
|
}
|
|
|
|
var codes: [String] { funds.map(\.code) }
|
|
|
|
func fund(code: String) -> WatchlistFund? {
|
|
funds.first { $0.code == code }
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private func load() {
|
|
guard let data = try? Data(contentsOf: storeURL) else { return }
|
|
do {
|
|
let decoded = try JSONDecoder.fundDecoder.decode([WatchlistFund].self, from: data)
|
|
self.funds = decoded.sorted(by: { $0.displayOrder < $1.displayOrder })
|
|
} catch {
|
|
NSLog("[fund-plugin] watchlist load failed: \(error)")
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
let snapshot = self.funds
|
|
queue.async { [storeURL] in
|
|
do {
|
|
let encoder = JSONEncoder.fundEncoder
|
|
let data = try encoder.encode(snapshot)
|
|
let tmp = storeURL.appendingPathExtension("tmp")
|
|
try data.write(to: tmp, options: .atomic)
|
|
_ = try? FileManager.default.replaceItemAt(storeURL, withItemAt: tmp)
|
|
} catch {
|
|
NSLog("[fund-plugin] watchlist save failed: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension JSONEncoder {
|
|
static var fundEncoder: JSONEncoder {
|
|
let e = JSONEncoder()
|
|
e.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
e.dateEncodingStrategy = .iso8601
|
|
return e
|
|
}
|
|
}
|
|
|
|
private extension JSONDecoder {
|
|
static var fundDecoder: JSONDecoder {
|
|
let d = JSONDecoder()
|
|
d.dateDecodingStrategy = .iso8601
|
|
return d
|
|
}
|
|
}
|