iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Mobile Development

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

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。


上一篇
D12 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數K線圖分析 }
下一篇
D14 - 用 Swift 和公開資訊,打造投資理財的 Apps { 加權指數K線圖實作.2 }
系列文
使用 Swift 和公開資訊,打造投資理財的 Apps37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言