b2970e4c-45a2-423e-b84d-257.../Sources/data/Models.swift

199 lines
6.7 KiB
Swift
Raw Permalink Normal View History

//
// 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
}
}
}