D25 - 用 Swift 和公开资讯,打造投资理财的 Apps { 三大法人成交比重 资料分析 }

台湾股市有揭露三大法人当日买进卖出的金额,在市场上会有流派依照这些进出的资讯,调整手上的资金部位。因为有一种说法是,当法人决定对某档个股买进,或是对台湾股市执行某向策略,在某个时期内,方向不会变。

不过...现在台股有当日冲销这样的机制,而且政府为了推行这个制度,在当冲税率上还有优惠,这个优惠政策目前看起来会持续到 2024 年。因为税率有优惠,而且现在台股的波动大,观察新闻可以看到很多「少年股神」的新闻。这些少年股神们的进出,是足以影响当日的波动。另外,也不是所有的法人进出大量都是长期布局的,也是有特定的外资法人交易的频率就是在当天敲大单买进,然後隔天倒出的。因为文章不想提及特定券商法人,建议可以蒐寻「隔日冲」、「券商」等关键字,你就会看到别人整理的隔日冲券商和特定分点资料。这边比较要注意的是特定分点的资料,就只是某帐户在该分点进出的量而,并不是那个分点自己下单,即使今天你看到特定分点下巨量的单,也不代表明天就会卖出。

资料下载页面

https://www.twse.com.tw/zh/page/trading/fund/BFI82U.html

如果要拿当前最新资料(英文版)

https://www.twse.com.tw/en/fund/BFI82U?response=csv&dayDate=&weekDate=&monthDate=&type=day

如果要拿某月某日的资料(英文版)

https://www.twse.com.tw/en/fund/BFI82U?response=csv&dayDate=20210914&weekDate=20210913&monthDate=20210915&type=day

因为这只 api 拿取资料的方式是一次一天,没办法像前面的 K 线资料,可以一次拿取多天的资料,所以使用一次 API 的 query,没办法把计算後的三大法人百分比,用 line chart 表示。

如果考虑目前拿到资料的方式,已经有目前大盘当日成交量,那就能算出[三大法人] / [非三大法人] 的比例。而呈现比例最方便的 chart 就是 pie chart,接下来,我们就用 pie chart 呈现三大法人占当日成交量的百分比。

先进行资料分析

https://ithelp.ithome.com.tw/upload/images/20211004/20140622fe8edhnA3u.png

需要的栏位是 [Total Buy] [Total Sell] [Difference]

如果不确定 item 中每一行代表的意义,可以下载一下中文的资料做对照。

https://ithelp.ithome.com.tw/upload/images/20211004/20140622igRdsoacib.png

DataModel 设计如下

struct MajorInvestor {

    let typeString: String
    let date: Date
    let totalBuyString: String
    let totalSellString: String
    let diffString: String
    
    private var numberFormatter: NumberFormatter {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter
    }
    
    var totalBuy: Double {
        return numberFormatter.number(from: totalBuyString)?.doubleValue ?? 0
    }
    
    var totalSell: Double {
        return numberFormatter.number(from: totalSellString)?.doubleValue ?? 0
    }
    
    var difference: Double {
        return numberFormatter.number(from: diffString)?.doubleValue ?? 0
    }
}

而日期的资讯,在第一行的第一个空白之前

所以我们需要把 yyyy/MM/dd 这一段 String 拿出来,再给 DateUtility 转成 Date 型别

    private func getFirstLine(_ string: String) -> String {
        return CSVAdapter.getFirstLine(string).replacingOccurrences(of: "\"", with: "")
    }
    
    private func getDateString(from firstLine: String) -> String {
        let separatedString = firstLine.components(separatedBy: " ")
        return separatedString.first ?? ""
    }

整个拉取资料的类别设计如下

import Foundation

//三大法人
//https://www.twse.com.tw/zh/page/trading/fund/BFI82U.html
//https://www.twse.com.tw/en/fund/BFI82U?response=csv&dayDate=&weekDate=&monthDate=&type=day
// 如果要 query 某个日期
//https://www.twse.com.tw/en/fund/BFI82U?response=csv&dayDate=20210914&weekDate=20210913&monthDate=20210915&type=day
class ThreeMajorInvestorsManager {
    
    private lazy var alamofireAdapter: AlamofireAdapter = {
        return AlamofireAdapter()
    }()
    
    func requestInvestorsInfo(completion: @escaping (([MajorInvestor], Error?) -> Void)) {
        
        let urlString = "https://www.twse.com.tw/en/fund/BFI82U?response=csv&dayDate=&weekDate=&monthDate=&type=day"
        
        alamofireAdapter.requestForString(urlString, method: .get) { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let string):
                
                let firstLine = self.getFirstLine(string)
                let dateString = self.getDateString(from: firstLine)
                let majorInvestors = self.convert(from: string, dateString: dateString)
                completion(majorInvestors, nil)
            case .failure(let error):
                completion([], error)
            }
        }
    }
    
    private func trimmedFirstLine(_ string: String) -> String {
        
        return CSVAdapter.removeLine(string, at: 1)
    }
    
    private func getFirstLine(_ string: String) -> String {
        return CSVAdapter.getFirstLine(string).replacingOccurrences(of: "\"", with: "")
    }
    
    private func getDateString(from firstLine: String) -> String {
        let separatedString = firstLine.components(separatedBy: " ")
        return separatedString.first ?? ""
    }
    
    private func convert(from string: String, dateString: String) -> [MajorInvestor] {
        
        var majorInvestors = [MajorInvestor]()
        
        let trimmedString = self.trimmedFirstLine(string)
        let dateUtility = DateUtility()
        
        if let csv = CSVAdapter(rawString: trimmedString),
           let date = dateUtility.getDate(from: dateString, format: "yyyy/MM/dd") {
            
            for each in csv.namedRows {
                
                let item = each["Item"] ?? ""
                let totalBuy = each["Total Buy"] ?? ""
                let totalSell = each["Total Sell"] ?? ""
                let difference = each["Difference"] ?? ""
                
                // 这一份 csv 档有 footer,如果 difference 没有字,就是 footer
                if !difference.isEmpty {
                    let majorInvestor = MajorInvestor(typeString: item, date: date, totalBuyString: totalBuy, totalSellString: totalSell, diffString: difference)
                    majorInvestors.append(majorInvestor)
                }
            }
        }
        
        return majorInvestors
    }
}

台股申购日历
IT铁人赛Demo App

下方是这次 D1 ~ D12 的完成品,可以下载来试
App Store - 台股申购日历

https://ithelp.ithome.com.tw/upload/images/20210924/20140622ypOBM0tgrZ.png


<<:  Day20 NiFi - 与 GCP Cloud Storage 对接设定

>>:  架构总览与闲聊

介绍Vertex(1) | ML#Day18

Day1 的时候有提到我们公司使用的云端方案是GCP (Google Cloud Platform)...

14【推坑】考 APCS 升大学大有优势

提完了那麽多有关 APCS 的事,这次想要分析考 APCS 能够有怎样的好处。 权威性: APCS ...

[Day - 30] 不完美的结束

最後,还是到了这最後一天,这第 30 天不完美的完赛,有时候时常都会想,上班就很忙了,开的 Tick...

[Day02] Flutter GetX VScode extension & tips

今天主要介绍VSCode开发Flutter时装哪些扩充插件, 还有一些开发时会用到的小眉角,写起来稍...