Files
vgearen 28eb6bd98c feat(korea): add Market.krStock + KRW currency + chart x-axis
Adding Market.krStock cascades into several exhaustive switches that must all
update together: ExchangeRates.convert, DisplayCurrency, StockChartView ranges
and labels. Bundles tasks 2/3/4/5/14 into one build-passing landing.

- Market enum: add krStock case + kr_ prefix routing in Market.from
- ExchangeRates: add usdToKrw (fallback 1380), krwToCny derived, KRW path in convert
- DisplayCurrency: add .krw (₩), display name 韩元
- CurrencyService.fetchRates: include fx_susdkrw in Sina FX batch URL
- StockChartView: KR fullDayRange 0...389 (KST 09:00-15:30, 390 min); KR axis labels
  09:00/10:30/12:00/13:30/15:30

Tests: +5 Stock cases for KR, +8 CurrencyKRWTests covering all KR↔CNY/USD/HKD/KRW
conversion paths and fallback constant.
2026-05-14 10:01:57 +08:00

64 lines
2.3 KiB
Swift

import Foundation
enum Market: String, Codable, CaseIterable {
case aStock = "A股"
case hkStock = "港股"
case usStock = "美股"
case krStock = "韩股"
static func from(code: String) -> Market {
if code.hasPrefix("hk") { return .hkStock }
if code.hasPrefix("usr_") { return .usStock }
if code.hasPrefix("kr_") { return .krStock }
return .aStock
}
}
struct Stock: Identifiable, Equatable {
var id: String // 股票代码,如 "sh600000"、"usr_aapl"、"hk00700"
var name: String
var market: Market
var costPrice: Double? // 持仓成本价(nil = 未设置)
var holdingShares: Double? // 持仓股数(nil = 未设置)
/// 持仓浮盈亏 = (当前价 - 成本价) × 股数;未设置持仓则返回 nil
func pnl(quote: Quote) -> Double? {
guard let cost = costPrice, let shares = holdingShares else { return nil }
return (quote.price - cost) * shares
}
/// 当日盈亏 = 涨跌额 × 股数;未设置股数则返回 nil
func dailyPnl(quote: Quote) -> Double? {
guard let shares = holdingShares else { return nil }
return quote.change * shares
}
/// 浮盈亏百分比 = (现价 - 成本价) / 成本价 × 100
func pnlPercent(quote: Quote) -> Double? {
guard let cost = costPrice, cost > 0 else { return nil }
return (quote.price - cost) / cost * 100
}
/// 当日盈亏百分比 = 涨跌额 / 成本价 × 100
func dailyPnlPercent(quote: Quote) -> Double? {
guard let cost = costPrice, cost > 0 else { return nil }
return quote.change / cost * 100
}
}
// MARK: - Codable(容错:未知字段用默认值,避免新版本解码老数据时整条失败)
extension Stock: Codable {
enum CodingKeys: String, CodingKey {
case id, name, market, costPrice, holdingShares
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
name = try c.decode(String.self, forKey: .name)
market = try c.decode(Market.self, forKey: .market)
costPrice = try c.decodeIfPresent(Double.self, forKey: .costPrice)
holdingShares = try c.decodeIfPresent(Double.self, forKey: .holdingShares)
}
}