iT邦幫忙

2021 iThome 鐵人賽

0
Mobile Development

使用 Swift 和公開資訊,打造投資理財的 Apps系列 第 32

D31 - 用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購功能擴充,算出價差.2}

上一篇,提到了可以在 tableView(_:willDisplay:forRowAt:) 中發動 URLRequest,這邏輯很正常,但真的不建議這麼做。

先來看上次的 API 接口

https://www.twse.com.tw/en/exchangeReport/STOCK_DAY?response=csv&date=20210928&stockNo=2330

我們要抽換的是 stockNo 後面的數字,而發動的時機,就是每次 tableViewCell 的 willDisplay。就這個畫面來說,大概在剛進入畫面的時候,就會發動五到六次不等的 API request。

而經過實測,這樣子發動 API Request ,會被證交所視為不正常的網路連線,然後會在接下來的一段時間,都無法拿到證交所的資訊,包含網頁也打不開。

既然這個方法走不通,那我們就要去找其他來源。去找是否有地方,可以一次性的下載所有上市股票的收盤價。

在政府的開放資料平台,是有這個資訊的。

https://data.gov.tw/dataset/11549

而下載所有資料的載點,也在這邊。

https://www.twse.com.tw/exchangeReport/STOCK_DAY_ALL?response=open_data

可拿取的資料如下圖

https://ithelp.ithome.com.tw/upload/images/20211011/20140622XJtaIApuox.png

先宣告資料模型 StockDayTick

import Foundation

/// 這個 Data Model 會被公開資訊頁 和 開放資料共用,兩者的資料有差,如果另一邊沒有的,會用 default string = "-" 處理掉
struct StockDayTick: Codable {
    
    private var dateUtility: DateUtility {
        return DateUtility()
    }
    
    private var numberFormatter: NumberFormatter {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter
    }
    
    /// 如果是全市場台股的 dateString, format 為 yyyyMMdd
    let dateString: String
    let stockCode: String
    
    /// 只有全市場台股的資料才有 stock name
    var stockName = ""
    
    /// 成交股數
    let volumeString: String
    
    /// 成交金額
    let valueString: String
    
    let openString: String
    let highestString: String
    let lowestString: String
    let closeString: String
    
    /// 與前天的漲跌差,但是在大盤的公開資訊,是沒有這個欄位的
    var change: String = ""
    
    /// 成交筆數,但是在大盤的公開資訊,是沒有這個欄位的
    var transaction: String = ""
    
    var date: Date? {
        return dateUtility.getDate(from: dateString, format: "yyyy/MM/dd")
    }
    
    var open: Double? {
        return numberFormatter.number(from: openString)?.doubleValue
    }
    
    var highest: Double? {
        return numberFormatter.number(from: highestString)?.doubleValue
    }
    
    var lowest: Double? {
        return numberFormatter.number(from: lowestString)?.doubleValue
    }
    
    var close: Double? {
        return numberFormatter.number(from: closeString)?.doubleValue
    }
    
    /// 大盤 K 線的建構式
    init(stockCode: String, stockName: String, dateString: String, openString: String, highestString: String, lowestString: String, closeString: String) {
        
        self.stockCode = stockCode
        self.stockName = stockName
        self.dateString = dateString
        self.openString = openString
        self.highestString = highestString
        self.lowestString = lowestString
        self.closeString = closeString
        
        self.volumeString = ""
        self.valueString = ""
    }
    
    init(dateString: String,
         stockCode: String,
         stockName: String = "",
         volumeString: String,
         valueString: String,
         openString: String,
         highestString: String,
         lowestString: String,
         closeString: String,
         change: String,
         transaction: String) {
        
        self.dateString = dateString
        self.stockCode = stockCode
        self.stockName = stockName
        self.volumeString = volumeString
        self.valueString = valueString
        self.openString = openString
        self.highestString = highestString
        self.lowestString = lowestString
        self.closeString = closeString
        self.change = change
        self.transaction = transaction
    }
}

下載資料的程式碼

struct StockDayPriceManager {
    
    private var alamofireAdapter: AlamofireAdapter {
        return AlamofireAdapter.shared
    }
    
    private var dateUtility: DateUtility {
        return DateUtility()
    }
    
    private var dateString: String {
        return dateUtility.getString(date: Date(), format: "yyyyMMdd")
    }
    
}

/// 這一區的程式碼,下載全台股所有個股單日行情,包含開高低收,成交筆數,成交股數,成交金額,價差
extension StockDayPriceManager {
    
    /// 這一道 csv 不用拿掉第一行
    func getAllTwMarketStockDayPrice(completion: @escaping ((Result<[StockDayTick], Error>) -> Void)) {
        
        let urlString = "https://www.twse.com.tw/exchangeReport/STOCK_DAY_ALL?response=open_data"
        
        alamofireAdapter.requestForStringWithRepsonseHeader(urlString) { string, allHeaders, error in
            
            if let dateString = getTwMarketDateString(from: allHeaders),
               let ticks = getTwMarketDayPriceList(rawString: string, dateString: dateString) {
                
                completion(.success(ticks))
            } else {
                completion(.failure(GetAllMarketError()))
            }
        }
    }
    
    private func getTwMarketDateString(from responseHeaders: [AnyHashable: Any]?) -> String? {
        
        if let headers = responseHeaders,
            let fileName = headers["Content-Disposition"] as? String {
            
            return fileName.slice(from: "STOCK_DAY_ALL_", to: ".csv")
        }
        
        return nil
    }
    
    private func getTwMarketDayPriceList(rawString: String, dateString: String) -> [StockDayTick]? {
        
        if let csv = CSVAdapter(rawString: rawString) {
            
            var ticks = [StockDayTick]()
            
            for row in csv.namedRows {
                
                let stockCode = row["證券代號"] ?? ""
                let stockName = row["證券名稱"] ?? ""
                let volume = row["成交股數"] ?? ""
                let value = row["成交金額"] ?? ""
                let open = row["開盤價"] ?? ""
                let highest = row["最高價"] ?? ""
                let lowest = row["最低價"] ?? ""
                let close = row["收盤價"] ?? ""
                let change = row["漲跌價差"] ?? ""
                let transaction = row["成交筆數"] ?? ""
                
                let tick = StockDayTick(dateString: dateString, stockCode: stockCode, stockName: stockName, volumeString: volume, valueString: value, openString: open, highestString: highest, lowestString: lowest, closeString: close, change: change, transaction: transaction)
                
                ticks.append(tick)
            }
            
            return ticks
        }
        
        return nil
    }
}

extension StockDayPriceManager {
    
    struct GetAllMarketError: LocalizedError {
        
        var errorDescription = "全市場K棒開高低收資料錯誤"
    }
}

而呼叫的時機點,可以自由決定,我目前是放在 AppDelegate didFinishLaunch

StockDayPriceManager().getAllTwMarketStockDayPrice { [weak self] result in
            switch result {
            case .success(let ticks):
                self?.save(twAllMarketTicks: ticks)
            case .failure(let error):
                Logger.log("拉取台股交易日全市場結果失敗: \(error.localizedDescription)")
            }
}

上一篇
D30 - 用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購功能擴充,算出價差 }
下一篇
D32 - 用 Swift 和公開資訊,打造投資理財的 Apps { 台股申購功能擴充,算出價差.3 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37

尚未有邦友留言

立即登入留言