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.
This commit is contained in:
vgearen
2026-05-14 10:01:57 +08:00
parent 36ec419416
commit 28eb6bd98c
6 changed files with 103 additions and 3 deletions
+3
View File
@@ -4,12 +4,14 @@ enum DisplayCurrency: String, Codable, CaseIterable {
case cny = "CNY"
case hkd = "HKD"
case usd = "USD"
case krw = "KRW"
var symbol: String {
switch self {
case .cny: return "¥"
case .hkd: return "HK$"
case .usd: return "$"
case .krw: return "₩"
}
}
@@ -18,6 +20,7 @@ enum DisplayCurrency: String, Codable, CaseIterable {
case .cny: return "人民币 ¥"
case .hkd: return "港币 HK$"
case .usd: return "美元 $"
case .krw: return "韩元 ₩"
}
}
}
+2
View File
@@ -4,10 +4,12 @@ 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
}
}
+9 -3
View File
@@ -1,11 +1,14 @@
import Foundation
struct ExchangeRates {
var usdToCny: Double = 7.28 // 默认兜底值
var usdToCny: Double = 7.28 // 默认兜底值
var usdToHkd: Double = 7.78
var usdToKrw: Double = 1380.0 // 默认兜底(2026 年初均值);CurrencyService 拉成功后覆盖
/// HKD/CNY = USD/CNY ÷ USD/HKD
var hkdToCny: Double { usdToCny / usdToHkd }
/// KRW/CNY = USD/CNY ÷ USD/KRW
var krwToCny: Double { usdToCny / usdToKrw }
/// 将 amount(native 货币)转换为目标货币
func convert(_ amount: Double, from market: Market, to currency: DisplayCurrency) -> Double {
@@ -15,21 +18,23 @@ struct ExchangeRates {
case .aStock: inCny = amount
case .hkStock: inCny = amount * hkdToCny
case .usStock: inCny = amount * usdToCny
case .krStock: inCny = amount * krwToCny
}
// 再从 CNY 转出去
switch currency {
case .cny: return inCny
case .hkd: return inCny / hkdToCny
case .usd: return inCny / usdToCny
case .krw: return inCny / krwToCny
}
}
}
final class CurrencyService {
/// 从新浪财经获取 USD/CNY、USD/HKD 汇率,失败时返回内置兜底值
/// 从新浪财经获取 USD/CNY、USD/HKD、USD/KRW 汇率,失败时返回内置兜底值
static func fetchRates() async -> ExchangeRates {
let urlStr = "https://hq.sinajs.cn/list=fx_susdcny,fx_susdhkd"
let urlStr = "https://hq.sinajs.cn/list=fx_susdcny,fx_susdhkd,fx_susdkrw"
guard let url = URL(string: urlStr) else { return ExchangeRates() }
var req = URLRequest(url: url)
req.setValue("https://finance.sina.com.cn", forHTTPHeaderField: "Referer")
@@ -41,6 +46,7 @@ final class CurrencyService {
for line in body.components(separatedBy: "\n") {
if let r = parseFirstPositiveDouble(from: line, code: "fx_susdcny") { rates.usdToCny = r }
if let r = parseFirstPositiveDouble(from: line, code: "fx_susdhkd") { rates.usdToHkd = r }
if let r = parseFirstPositiveDouble(from: line, code: "fx_susdkrw") { rates.usdToKrw = r }
}
return rates
}
+4
View File
@@ -22,6 +22,7 @@ struct StockChartView: View {
case .aStock: return 0...239
case .hkStock: return 0...329
case .usStock: return 0...959
case .krStock: return 0...389
}
}
@@ -35,6 +36,9 @@ struct StockChartView: View {
case .usStock:
// 基准 04:00 ET;330=09:30;720=16:00;959=19:59
return [(0, "04:00"), (330, "09:30"), (570, "13:30"), (720, "16:00"), (959, "20:00")]
case .krStock:
// 基准 09:00 KST;180=12:00;389=15:29
return [(0, "09:00"), (90, "10:30"), (180, "12:00"), (270, "13:30"), (389, "15:30")]
}
}