mirror of
https://github.com/carey314/mio-plugin-fund.git
synced 2026-08-10 05:44:31 +00:00
199 lines
6.7 KiB
Swift
199 lines
6.7 KiB
Swift
|
|
//
|
|||
|
|
// Models.swift
|
|||
|
|
// 盯基金 plugin v0.2 (UI redesign per design spec)
|
|||
|
|
//
|
|||
|
|
// Models grew from "watchlist with name+code" to "actual holding with
|
|||
|
|
// cost basis and shares" so the design's hero card (总市值 / 今日盈亏 /
|
|||
|
|
// 累计盈亏) can compute real numbers. Cost/shares are optional — if
|
|||
|
|
// empty, the row falls back to "watchlist only" display (rate %
|
|||
|
|
// without ¥ amounts), so we don't force every user to type cost basis
|
|||
|
|
// on day one.
|
|||
|
|
//
|
|||
|
|
|
|||
|
|
import Foundation
|
|||
|
|
|
|||
|
|
// MARK: - Fund
|
|||
|
|
|
|||
|
|
/// One fund the user has subscribed to (lives in the watchlist).
|
|||
|
|
/// `shares` and `costNav` are optional: when set, the holdings hero
|
|||
|
|
/// card shows real ¥ P&L; when nil, only the rate % displays.
|
|||
|
|
struct WatchlistFund: Codable, Equatable, Identifiable {
|
|||
|
|
let code: String // "005827"
|
|||
|
|
let name: String // "易方达蓝筹精选混合"
|
|||
|
|
var addedAt: Date
|
|||
|
|
var displayOrder: Int
|
|||
|
|
|
|||
|
|
/// Shares held (份额). Optional — nil means "I just want to watch this fund".
|
|||
|
|
var shares: Double?
|
|||
|
|
/// Average cost basis per share (持仓成本/单位净值).
|
|||
|
|
var costNav: Double?
|
|||
|
|
|
|||
|
|
var id: String { code }
|
|||
|
|
|
|||
|
|
/// True iff the user has filled in both shares + cost basis.
|
|||
|
|
var hasPosition: Bool {
|
|||
|
|
guard let s = shares, s > 0, let c = costNav, c > 0 else { return false }
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Cost amount = shares × cost basis (only valid when hasPosition).
|
|||
|
|
var costAmount: Double? {
|
|||
|
|
guard let s = shares, let c = costNav else { return nil }
|
|||
|
|
return s * c
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Today's intraday estimate + last-published NAV for one fund.
|
|||
|
|
/// Returned by `FundClient.estimate(for:)` (the
|
|||
|
|
/// `fundgz.1234567.com.cn/js/{code}.js` JSONP feed).
|
|||
|
|
struct FundEstimate: Equatable {
|
|||
|
|
let code: String
|
|||
|
|
let name: String
|
|||
|
|
let publishedDate: Date // jzrq — date the published NAV applies to
|
|||
|
|
let publishedNav: Double // dwjz — last published unit NAV
|
|||
|
|
let estimatedNav: Double? // gsz — intraday estimated NAV (nil out of session)
|
|||
|
|
let estimatedRate: Double? // gszzl — intraday %, e.g. 0.53 means +0.53%
|
|||
|
|
let estimatedAt: Date? // gztime — last time estimate refreshed
|
|||
|
|
|
|||
|
|
/// "Best available" NAV — use intraday estimate if we have it, else
|
|||
|
|
/// fall back to last published. The hero card uses this to compute
|
|||
|
|
/// market value during trade hours.
|
|||
|
|
var bestNav: Double { estimatedNav ?? publishedNav }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// One row from the historical NAV table (used for sparkline).
|
|||
|
|
struct FundNavPoint: Equatable {
|
|||
|
|
let date: Date
|
|||
|
|
let unitNav: Double
|
|||
|
|
let dailyRate: Double
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// One match from the eastmoney fund-suggest endpoint.
|
|||
|
|
/// Includes "category" (fund type) and a heuristic "tag" for the
|
|||
|
|
/// design's coloured chip.
|
|||
|
|
struct FundSearchHit: Equatable, Identifiable {
|
|||
|
|
let code: String
|
|||
|
|
let name: String
|
|||
|
|
let category: String? // e.g. "混合型-灵活" / "债券型-混合二级"
|
|||
|
|
var id: String { code }
|
|||
|
|
|
|||
|
|
/// Coarse-grained tag derived from category for the design's coloured
|
|||
|
|
/// chip in the search list. Not authoritative — just hint colour.
|
|||
|
|
var displayTag: String {
|
|||
|
|
guard let c = category?.lowercased() else { return "基金" }
|
|||
|
|
if c.contains("指数") { return "指数" }
|
|||
|
|
if c.contains("股票") { return "股票" }
|
|||
|
|
if c.contains("混合") { return "混合" }
|
|||
|
|
if c.contains("债券") { return "债券" }
|
|||
|
|
if c.contains("货币") { return "货币" }
|
|||
|
|
if c.contains("qdii") { return "QDII" }
|
|||
|
|
if c.contains("etf") { return "ETF" }
|
|||
|
|
return "基金"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Gold
|
|||
|
|
|
|||
|
|
/// Gold realtime quote. Same struct services SHFE 沪金 (RMB/g, used as
|
|||
|
|
/// the chart price) and the foreign reference quotes (COMEX/London,
|
|||
|
|
/// USD/oz with RMB/g conversion alongside).
|
|||
|
|
struct GoldQuote: Equatable, Identifiable {
|
|||
|
|
enum Source: String, Codable, Hashable, CaseIterable {
|
|||
|
|
case shanghaiSpot = "shanghai_spot"
|
|||
|
|
case shfeFutures = "shfe_futures"
|
|||
|
|
case comex = "comex"
|
|||
|
|
case london = "london"
|
|||
|
|
|
|||
|
|
var displayName: String {
|
|||
|
|
switch self {
|
|||
|
|
case .shanghaiSpot: return "上海金"
|
|||
|
|
case .shfeFutures: return "沪金"
|
|||
|
|
case .comex: return "纽约金"
|
|||
|
|
case .london: return "伦敦金"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Sina symbol for `https://hq.sinajs.cn/list={symbol}`
|
|||
|
|
var sinaSymbol: String {
|
|||
|
|
switch self {
|
|||
|
|
case .shanghaiSpot: return "AU0"
|
|||
|
|
case .shfeFutures: return "AU0"
|
|||
|
|
case .comex: return "hf_GC"
|
|||
|
|
case .london: return "hf_XAU"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let source: Source
|
|||
|
|
let last: Double
|
|||
|
|
/// Today's opening price. Distinct from `prevClose` — UI's "今开"
|
|||
|
|
/// row needs this, not yesterday's close.
|
|||
|
|
let open: Double?
|
|||
|
|
let prevClose: Double?
|
|||
|
|
let high: Double?
|
|||
|
|
let low: Double?
|
|||
|
|
let updatedAt: Date?
|
|||
|
|
/// Set when source ∈ {comex, london} so UI can show ¥/g alongside USD/oz.
|
|||
|
|
let rmbPerGram: Double?
|
|||
|
|
var id: String { source.rawValue }
|
|||
|
|
|
|||
|
|
var change: Double? {
|
|||
|
|
guard let p = prevClose else { return nil }
|
|||
|
|
return last - p
|
|||
|
|
}
|
|||
|
|
var changeRate: Double? {
|
|||
|
|
guard let p = prevClose, p != 0 else { return nil }
|
|||
|
|
return (last - p) / p * 100.0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// One day's OHLCV for the gold K-line chart.
|
|||
|
|
struct GoldDailyBar: Equatable {
|
|||
|
|
let date: Date
|
|||
|
|
let open: Double
|
|||
|
|
let high: Double
|
|||
|
|
let low: Double
|
|||
|
|
let close: Double
|
|||
|
|
let volume: Double
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// User's gold holding position. Both fields optional so an empty
|
|||
|
|
/// holding is valid (you might just want to watch the price without
|
|||
|
|
/// committing). Persisted as JSON next to the watchlist.
|
|||
|
|
struct GoldPosition: Codable, Equatable {
|
|||
|
|
/// Total grams held (克). Nil = no holding entered yet.
|
|||
|
|
var grams: Double?
|
|||
|
|
/// Average buy-in cost per gram (元/克).
|
|||
|
|
var costPerGram: Double?
|
|||
|
|
|
|||
|
|
var hasPosition: Bool {
|
|||
|
|
guard let g = grams, g > 0, let c = costPerGram, c > 0 else { return false }
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var costAmount: Double? {
|
|||
|
|
guard let g = grams, let c = costPerGram else { return nil }
|
|||
|
|
return g * c
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Time range for the gold chart tab strip (1月/3月/1年/全部).
|
|||
|
|
enum GoldRange: String, CaseIterable, Identifiable {
|
|||
|
|
case oneMonth = "1月"
|
|||
|
|
case threeMonth = "3月"
|
|||
|
|
case oneYear = "1年"
|
|||
|
|
case all = "全部"
|
|||
|
|
|
|||
|
|
var id: String { rawValue }
|
|||
|
|
|
|||
|
|
/// Number of days to slice from the daily history. `nil` = all.
|
|||
|
|
var days: Int? {
|
|||
|
|
switch self {
|
|||
|
|
case .oneMonth: return 22 // ~22 trading days/month
|
|||
|
|
case .threeMonth: return 66
|
|||
|
|
case .oneYear: return 250
|
|||
|
|
case .all: return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|