D13 - 用 Swift 和公开资讯,打造投资理财的 Apps { 加权指数K线图实作.1 }

目标: 做出台湾加权指数 K 线图

之前做出来的台股申购是独立的功能,为了不影响前面已经完成的功能,所以开一个新的 VC 进行 K 线实作。架构一样,遵照 Apple 的 MVC 规范。

先开 Model

import Foundation

class TwStockMarketKLineModel {
    
}

然後再开 VC

import UIKit

class TwStockMarketKLineViewController: UIViewController {
    
    private lazy var model: TwStockMarketKLineModel = {
        let model = TwStockMarketKLineModel()
        return model
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

如果你不想让 storyboard 上的 VC 太多,那你可以让一系列相关的 VC 在一个 storyboard 上。所以我们另外开一个 storyboard 来放这个 VC。

https://ithelp.ithome.com.tw/upload/images/20210922/20140622UqaYf2MzVp.png

K 线的拿取网址如下

加权指数-公开资讯观测站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html

加权指数的 K 线的 csv 档位址
https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907

因为加权指数的栏位和申购资讯相比少很多,而且我只需要[日期] [开] [高] [低] [收] 而已,所以就直接拿英文版的资料,减少转换 Big5 的步骤。

因为需要 date=20210907 这一种日期格式,所以需要扩充 DateUtility 功能。

// 扩充在 DateUtility 里面,在拿取 K 线的时候要输入这个值
func getString(date: Date, format: String = "yyyy-MM-dd") -> String {
        
        DateUtility.dateFormatter.calendar = isoCalendar
        DateUtility.dateFormatter.dateFormat = format
        
        return DateUtility.dateFormatter.string(from: date)
    }

然後再新建一个 TwStockKLineManager

import Foundation

//加权指数-公开资讯观测站 https://www.twse.com.tw/zh/page/trading/indices/MI_5MINS_HIST.html
// https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907
class TwStockKLineManager {
    
    private var dateUtility: DateUtility {
        return DateUtility()
    }
    
    private lazy var alamofireAdapter: AlamofireAdapter = {
        return AlamofireAdapter()
    }()
    
    func requestTwStockKLine(date: Date, completion: @escaping ((Result<String, Error>) -> Void)) {
        
        let format = "yyyyMMdd"
        let string = dateUtility.getString(date: date, format: format)
        
        let urlString = "https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=\(string)"
        
        alamofireAdapter.requestForString(urlString, method: .get) { result in
            
            switch result {
            case .success(let string):
                completion(.success(string))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

这样,专案中的任何物件,只要呼叫 requestTwStockKLine ,就可以拿到加权指数的 csv 档。

我们先看一下载的 URL 和拿到的资料,可以看到後面的 date 需要输入的格式是 yyyyMMdd

https://www.twse.com.tw/en/indicesReport/MI_5MINS_HIST?response=csv&date=20210907

所得到的 response CSV 档,用 Number 或 Excel 打开会长这样。

https://ithelp.ithome.com.tw/upload/images/20210922/20140622LGVh6eL9M0.png

分析:

  • 我们要拿全部的栏位 [Date], [Opening Index], [Highest Index], [Lowest Index], [Closing Index]
  • 这一份 csv 档下载英文版就会是 UTF8 编码,下载中文版的会是 Big5 编码,所以下载英文版在开发上比较快,也比较安全。栏位用中文是一个很恐怖的事情。
  • csv 档第一行也要去掉,但 CSVAdapter 已经写好 func 了,直接使用就行。
  • Date 格式和申购资料的 csv 不同,用的是西元年,而不是民国年。这边的 DateUtiliy 在解析的时候,使用 isoCalendar 就行了。
  • 把日期换成 20210901、20210902,都会拿到一样的 csv 档案。但输入 20210801、20210831,会拿到八月份的资料,所以只要换成 n 年 n 月的第一天,你就可以拿到该月份的资料。
  • 单纯拿当月的资料,有可能资料量不够,以 0906 为例,只有 4 天的 K 棒。很难判断出有效的资讯,所以除了当月的资料以外,还要拿上个月的资料,这样 K 线的数量才能达到可分析的程度。

扩充 DateUtility - 得到距离现在 n 月第一天的 Date

如同之前 [台股申购实作.4] 所提到的。千万不要自己手动转换历法。千万不要自己手动转换历法!!千万不要自己手动转换历法!!!!

「正常来说」,每个月的月份天数,有 28 天,29 天,30 天,31 天。所以当下的日期减去 31 天,就绝对不会在这个月,也不能保证就会是「上个月」,如果真的这样实作,会有可能出现意料之外的日期。所以,请用 Foundation 中的功能,去找上个月的日期,这比自己写日期转换的错误率低非常非常多。

在 DateUtility 扩充 func,输入 Date,可以得到输入 Date 当月的第一天 Date

//得到这个月第一天的 Date
func getStartOfMonth(date: Date = Date()) -> Date {
        
        let calendar = isoCalendar
        let startDate = isoCalendar.startOfDay(for: date)
        return calendar.date(from: calendar.dateComponents([.year, .month], from: startDate)) ?? Date()
    }

而 Foundation 的 Date 相关操作中,就有能对某个 DateComponent 进行加减操作的 API。

Apple 说明文件 https://developer.apple.com/documentation/foundation/calendar/2293453-date

程序码

/// Returns a new `Date` representing the date calculated by adding an amount of a specific component to a given date.
    ///
    /// - parameter component: A single component to add.
    /// - parameter value: The value of the specified component to add.
    /// - parameter date: The starting date.
    /// - parameter wrappingComponents: If `true`, the component should be incremented and wrap around to zero/one on overflow, and should not cause higher components to be incremented. The default value is `false`.
    /// - returns: A new date, or nil if a date could not be calculated with the given input.
    @available(iOS 8.0, *)
    public func date(byAdding component: Calendar.Component, value: Int, to date: Date, wrappingComponents: Bool = false) -> Date?

所以,得到上个月第一天的 Date,就是先呼叫 getStartOfMonth(),取得当月第一天後,再用 dateComponent 的计算,拿取上个月第一天。

//取得输入 Date 的前一个月的第一天 Date
func getLastMonthStartDate(date: Date = Date()) -> Date {
        
        let calendar = isoCalendar
        let startOfMonth = getStartOfMonth(date: date)
        
        return calendar.date(byAdding: DateComponents(month: -1), to: startOfMonth) ?? Date()
    }

这样,完成了 Model 在拿取台股加权指数资料的时候,只要使用 DateUtility 的 func 就可以得到要输入的 Date。


<<:  [Day 22] 针对API的单元测试(二)

>>:  DAY11 - 第一个小范例 : LineBot 自动回话

[2020铁人赛Day29]糊里糊涂Python就上手-Pandas的观念与运用(下)

今日目标 学习了解 Python Pandas 资料存取与运用视觉化呈现数据 DataFrame 资...

Angular Reactive Form 表单 setValue 与 patchValue 差异

今天就来说说 setValue 与 patchValue 差异这部份吧 一开始不太能理解这 setV...

Longest Increasing Subsequence (最长递增子序列)

记录学习内容。看网路上大大们的文章和影片,做些纪录。 还不了解,内容可能有错误。 Longest I...

【D18】尝试料理:取得所有股票清单

前言 有了这些功能後,想要知道能不能跑所有的股票,然後做这些事情,无论是行情订阅,还是历史资料。因此...

#19 Telegram Bot 起手式

今天开始做我们的 Telegram Bot! Telegram Telegram 是一个通讯软件,就...