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)")
            }
}

<<:  25 - Stylelint - Lint CSS 程序码

>>:  DAY27 CNN(卷积神经网路 续一)

大共享时代系列_029_共享读书趣

是不是该读点书了呢? 参加读书会的好处? 打造社群学习RSC的价值:Reading(共读)、Shar...

那些被忽略但很好用的 Web API / 前言

Web API -- Application Programming Interface for ...

Day-5 注解与断行

一开始写code 最常使用的就是注解与断行,着解释为了方便标记説明想法,断行是为了更好阅读。 注解 ...

Day2 # Hello World

在第一天完成安装後,就可以使用 Go 来写程序啦! 作为一个工程师,一定要来段 Hello Worl...

DAY16:Pytorch transforms(上)

torchvision.transforms transforms可以用来改变样本的多样性,例如:旋...