diff --git a/StockMonitor/Models/AppSettings.swift b/StockMonitor/Models/AppSettings.swift index 9c7aa08..316cd1b 100644 --- a/StockMonitor/Models/AppSettings.swift +++ b/StockMonitor/Models/AppSettings.swift @@ -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 "韩元 ₩" } } } diff --git a/StockMonitor/Models/Stock.swift b/StockMonitor/Models/Stock.swift index 589beed..3e1eaa6 100644 --- a/StockMonitor/Models/Stock.swift +++ b/StockMonitor/Models/Stock.swift @@ -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 } } diff --git a/StockMonitor/Services/CurrencyService.swift b/StockMonitor/Services/CurrencyService.swift index 6696b69..98d56cc 100644 --- a/StockMonitor/Services/CurrencyService.swift +++ b/StockMonitor/Services/CurrencyService.swift @@ -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 } diff --git a/StockMonitor/Views/StockChartView.swift b/StockMonitor/Views/StockChartView.swift index 85baaba..210c163 100644 --- a/StockMonitor/Views/StockChartView.swift +++ b/StockMonitor/Views/StockChartView.swift @@ -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")] } } diff --git a/StockMonitorTests/Models/StockTests.swift b/StockMonitorTests/Models/StockTests.swift index 9f34222..1314415 100644 --- a/StockMonitorTests/Models/StockTests.swift +++ b/StockMonitorTests/Models/StockTests.swift @@ -54,4 +54,30 @@ final class StockTests: XCTestCase { changePercent: -10.0, updateTime: "15:00:00") XCTAssertEqual(stock.pnl(quote: quote)!, -500.0, accuracy: 0.001) } + + func test_market_fromCode_krStock_kospi() { + XCTAssertEqual(Market.from(code: "kr_005930.ks"), .krStock) + } + + func test_market_fromCode_krStock_kosdaq() { + XCTAssertEqual(Market.from(code: "kr_293490.kq"), .krStock) + } + + func test_market_krStock_rawValue() { + XCTAssertEqual(Market.krStock.rawValue, "韩股") + } + + func test_market_allCases_includes_kr() { + XCTAssertTrue(Market.allCases.contains(.krStock)) + } + + func test_stock_codable_roundtrip_kr() throws { + let stock = Stock(id: "kr_005930.ks", name: "Samsung Electronics", market: .krStock, + costPrice: 80000.0, holdingShares: 10) + let data = try JSONEncoder().encode(stock) + let decoded = try JSONDecoder().decode(Stock.self, from: data) + XCTAssertEqual(decoded.id, "kr_005930.ks") + XCTAssertEqual(decoded.market, .krStock) + XCTAssertEqual(decoded.costPrice, 80000.0) + } } diff --git a/StockMonitorTests/Services/CurrencyKRWTests.swift b/StockMonitorTests/Services/CurrencyKRWTests.swift new file mode 100644 index 0000000..e1fad20 --- /dev/null +++ b/StockMonitorTests/Services/CurrencyKRWTests.swift @@ -0,0 +1,59 @@ +import XCTest +@testable import Stockbar + +final class CurrencyKRWTests: XCTestCase { + + private func rates() -> ExchangeRates { + var r = ExchangeRates() + r.usdToCny = 7.28 + r.usdToHkd = 7.78 + r.usdToKrw = 1380.0 + return r + } + + func test_default_usdToKrw_isFallback() { + let r = ExchangeRates() + XCTAssertEqual(r.usdToKrw, 1380.0, accuracy: 0.0001) + } + + func test_convert_krw_to_cny() { + let r = rates() + let cny = r.convert(1_000_000, from: .krStock, to: .cny) + XCTAssertEqual(cny, 1_000_000.0 / 1380.0 * 7.28, accuracy: 0.01) + } + + func test_convert_krw_to_usd() { + let r = rates() + let usd = r.convert(1_380_000, from: .krStock, to: .usd) + XCTAssertEqual(usd, 1000.0, accuracy: 0.001) + } + + func test_convert_krw_to_hkd() { + let r = rates() + let hkd = r.convert(1_380_000, from: .krStock, to: .hkd) + XCTAssertEqual(hkd, 1000.0 * 7.78, accuracy: 0.01) + } + + func test_convert_krw_to_krw_identity() { + let r = rates() + XCTAssertEqual(r.convert(123_456, from: .krStock, to: .krw), 123_456.0, accuracy: 0.001) + } + + func test_convert_cny_to_krw() { + let r = rates() + let krw = r.convert(728.0, from: .aStock, to: .krw) + XCTAssertEqual(krw, 138_000.0, accuracy: 0.1) + } + + func test_convert_usd_to_krw() { + let r = rates() + let krw = r.convert(100.0, from: .usStock, to: .krw) + XCTAssertEqual(krw, 138_000.0, accuracy: 0.1) + } + + func test_convert_hkd_to_krw() { + let r = rates() + let krw = r.convert(778.0, from: .hkStock, to: .krw) + XCTAssertEqual(krw, 138_000.0, accuracy: 0.5) + } +}