[VBA] 自動上網抓取 股權分散表 — 透析大戶的佈局
目錄 :
何謂「 股權分散表 」?
※ 所謂「 股權分散表 」: 是由 臺灣集中保管結算所( 簡稱 : 集保結算所 / 集保中心 TDCC ),於每週六上午公布每檔股票之持股人數( 股東 ),並列表顯示各級別的持股單位,及持有比例的報表資料。
[ 股權分散表 ] : 以台泥( 1101 )為例 —
股權分散表 查詢網址
※ 集保結算所 同時提供查詢網址,方便投資者查詢各檔股票之股權分散表歷史資料。( PS. 一年期間內資料。)
◎ 股權分散表 查詢網址 : https://www.tdcc.com.tw/portal/zh/smWeb/qryStock
[ 股權分散表 查詢網站 ] :
◎ Goodinfo! 台灣股市資訊網 : https://goodinfo.tw/tw/EquityDistributionClassHis.asp?STOCK_ID=0050&CHT_CAT=WEEK
◎ 神秘金字塔 : https://norway.twsthr.info/StockHolders.aspx
股權分散表 更新時間
◎ 股權分散表更新時間 — 每周六 上午 09 : 00 後。
解讀 股權分散表
※ 股權分散表包含 : 股東人數 / 持股數量 / 持股比例 等信息。
◎ 在解讀這些數據時,投資人必須注意以下三點 :
[ 1 ] : 主要股東:持有大量股份的股東,例如 : 機構投資者 / 大股東 / 內部人士。他們的持股動向會對公司的經營和股價,產生一定影響。
[ 2 ] : 股權集中度:隨時觀察股權分佈的集中程度。如果主要股東(大戶)持有大部分股份,那麼公司股價就容易受到拉抬,反之,太多的游離散戶持有,股價要往上的機率,就相對的困難許多。
[ 3 ] : 股權變動:關注主要股東股權持有量和比例的變化。這些變動表示投資者對公司的信心和利益的變化。
「 大戶 」的定義
※ 在股票新手教學 — 籌碼面 文章中有介紹,擁有公司股票千張以上的人,謂之「 大戶 」。
◎ 本文,大戶的定義 : 則以股價高低,再加以細分,分類如下 :
[ 1 ] : 股價 < 50元 : 持股超過 1000張 以上。
[ 2 ] : 100元 > 股價 < 50元 : 持股超過 600張 以上。
[ 3 ] : 股價 > 100元 : 持股超過 400張 以上。
( PS. 至於持股多少為大戶,也沒一定準則,但可以利用程式來驗證各檔股票走勢,再作定義。)
VBA實作 — 自動上網抓取股權分散表 to Excel
※ 要完成 自動上網抓取股權分散表 to Excel,可將其分成二步驟 :
[ 1 ] : 下載股權分散表
[ 2 ] : 股權分散表資料清理
下載股權分散表
前置作業
[ 前置作業 ] :
[ 1 ] : 新增活頁簿 — 設定檔名 : 2023_股權分散表.xlsm。
[ 2 ] : 新增2個工作表 — 設定名稱 : 股權分散表 及 整體CSV。
[ 3 ] : 在股權分散表工作表 A1 欄位,填上 : ” 股權分散表 網址 : ” 文字標題。
[ 參考圖 ] :
CSV檔 — 下載方法
※ 集保結算所不僅提供各檔股票股權分散表的查詢網頁;同時,也貼心地將所有股票的股權資料,整合成股權分散表CSV檔,方便投資人下載使用。
[ 下載方法 ] :
1. 進入股權分散表頁面後,將頁面往下拉,會發現「股權分散表CSV檔」的下載位置。( PS. 參考下圖 )
2. 使用滑鼠右鍵,點擊「股權分散表」後,會出現功能選項視窗,點選 : 複製連結網址 ( 取得網址字串 : https://opendata.tdcc.com.tw/getOD.ashx?id=1-5 )。
3. 點選 : 股權分散表工作表 B1 欄位,按 : 滑鼠右鍵,選擇 : 連結 — 最近的項目內的網址 選項,便可將 下載股權分散表CSV檔 的 超連結,貼在 B1 欄位上,其目的是 — 要讓之後的VBA程式,方便點擊,自動下載股權分散表CSV檔。
4. 超連結 : 下載股權分散表CSV檔,設定完成。
儲存格 點擊 超連結
◎ B2 欄位設定好超連結後,便可開始設計VBA程式,來點擊觸發 — 下載股權分散表CSV檔。
[ 程式碼 / 點擊超連結 ]
Sheets("股權分散表").Select
Range("B1").Hyperlinks(1).Follow ' 點擊 -- 執行B1欄超連結
[ 語法解析 ] :
◎ Hyperlinks(1).Follow : 其中 Hyperlinks(1) 是指連結第1個設定的超連結,若有2個以上,就設定成 (2) (3) …… 等。( PS. Follow 是固定用法,觸發點擊的意思。)
錄製巨集 / 下載股權分散表CSV檔
[ 錄製巨集 — 操作步驟 ] :
A. 錄製巨集 : 若不了解如何錄製,請參考 錄製巨集(舊版) 內的錄製步驟,步驟做到 開始錄製 即可,因為這次要做的是 下載CSV檔 的資料。
B. 從文字/CSV : 確定開始錄製巨集後,游標點選功能表 : 資料,再點選工具列中的 從文字/CSV 選項。
C. 匯入資料 : 此時,會出現 匯入資料 的視窗,選取下載的 股權分散表CSV檔 ( 檔名為 : TDCC_OD_1-5.csv ),再執行 匯入 動作。
( PS. 若您程式下載位置,與我不同,可參考 : EXCEL功能筆記 — 查詢檔案下載位置,找出您的檔案位置。)
D. 載入CSV資料 : 隨即開啟載入畫面,並於右下角按下 : 載入 按鈕旁的 ▼ (下拉式選單),選擇 : 載入至。
E. 資料匯入儲存格 : 再於開啟的 匯入資料 視窗中,將欲下載之CSV檔案資料放在 整體CSV 工作表中,開始存放的位置設在 : A1 欄位。
F. 停止錄製巨集 : 完成下載動作後,一定要記得停止錄製巨集。
[ 資料下載完成圖 ] :
[ 程式碼 / 下載股權分散表CSV檔 ]
Application.CutCopyMode = False
ActiveWorkbook.Queries.Add Name:="TDCC_OD_1-5", Formula:= _
"let" & Chr(13) & "" & Chr(10) & " 來源 = Csv.Document(File.Contents(""C:\Users\YANG\Downloads\TDCC_OD_1-5.csv""),[Delimiter="","", Columns=6, Encoding=65001, QuoteStyle=QuoteStyle.None])," & Chr(13) & "" & Chr(10) & " 已將標頭升階 = Table.PromoteHeaders(來源, [PromoteAllScalars=true])," & Chr(13) & "" & Chr(10) & " 已變更類型 = Table.TransformColumnTypes(已將標頭升階,{{""資料日期"", Int64.Type}, {""證券代號"", Int64.Type}, {""持股分級"", Int64.Type}, {""人數"", Int64.Type}," & _
" {""股數"", Int64.Type}, {""占集保庫存數比例%"", type number}})" & Chr(13) & "" & Chr(10) & "in" & Chr(13) & "" & Chr(10) & " 已變更類型" & _
""
' ActiveSheet.ListObjects("表格_外部資料_1").Name = "表格_TDCC_OD_1_5"
With ActiveSheet.ListObjects.Add(SourceType:=0, Source:= _
"OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=TDCC_OD_1-5;Extended Properties=""""" _
, Destination:=Range("$A$1")).QueryTable
.CommandType = xlCmdSql
.CommandText = Array("SELECT * FROM [TDCC_OD_1-5]")
.RowNumbers = False
.FillAdjacentFormulas = False
.PreserveFormatting = True
.RefreshOnFileOpen = False
.BackgroundQuery = True
.RefreshStyle = xlInsertDeleteCells
.SavePassword = False
.SaveData = True
.AdjustColumnWidth = True
.RefreshPeriod = 0
.PreserveColumnInfo = True
.ListObject.DisplayName = "表格_TDCC_OD_1_5"
.Refresh BackgroundQuery:=False
End With
[ 注意 ] : 首次錄製下載CSV檔程式,不會出現問題;但再次執行上面程式碼時,便會出現錯誤訊號,卡在 ActiveSheet.ListObjects(“表格_外部資料_1”).Name = “表格_TDCC_OD_1_5” 這一段指令。此時,只要將其註解( ‘ ) 掉,就沒事了。
切斷查詢連線
※ 執行 從文字/CSV 指令,同樣會產生 查詢連線 (如下圖)。若不予以刪除,再次執行時,便會產生錯誤,這樣就無法達成自動化操作。
[ 查詢連線 — 連線情況 ] : 必須將其切斷。
[ 程式碼 / 切斷查詢連線 ] :
ActiveWorkbook.Queries(“TDCC_OD_1-5“).Delete
刪除股權分散表CSV檔
※ 為何要刪除已下載的股權分散表( TDCC_OD_1-5.csv ) ?
[ 答 ] : 那是因為不刪除原CSV檔(TDCC_OD_1-5.csv),之後新下載的CSV檔,會以 TDCC_OD_1-5.csv(1) … (2) … 依序遞增,但實際程式依然是抓取 TDCC_OD_1-5.csv,便會造成數據錯誤。
[ 程式碼 / 刪除股權分散表CSV檔 ] :
Kill “C:\Users\YANG\Downloads\TDCC_OD_1-5.csv” ‘ 刪除股權分散表CSV檔
股權分散表資料清理
※ 股權分散表資料下載至Excel,會發現很多不需要的股票資料,比如 : 00815 / 00683U / YFN6 …… 等。所以必須將其清理,只留下二位數( 50 ,也就是 : 0050 ),還有四位數的股票資料。
前置作業
[ 前置作業 ] :
[ 1 ] : 新增工作表 — 設定名稱 : 股權分散表資料清理。
[ 2 ] : 在 股權分散表資料清理 工作表,填上 : 各級別股東之持股張數 的文字標題。( 請參考下圖 )
股權分散表資料清理
◎ 同前言所說,股權分散表資料只需留下二和四位數資料,便只能使用VBA程式,來控制並刪除不需要的股票資料。
[ 程式碼 / 股權分散表資料清理 ] :
Sub 股權分散表資料清理()
Dim dataRow As Long, xCode As Integer
Sheets("股權分散表資料清理").Select
[A3:W3000].ClearContents ' 清除舊資料(保留格式)
newRow = [A65536].End(xlUp).Offset(1).Row
dataRow = Sheets("整體CSV").[A65536].End(xlUp).Row
[A1] = Sheets("整體CSV").[A2] ' 日期
For xStock = 2 To dataRow Step 17
xCode = Len(Sheets("整體CSV").Range("B" & xStock)) ' 查驗代號字元數
If xCode = 2 Then
Range("A" & newRow) = "00" & Sheets("整體CSV").Range("B" & xStock)
Call 記錄各級別股東持股張數
ElseIf xCode = 4 Then
If IsNumeric(Sheets("整體CSV").Range("B" & xStock)) Then
Range("A" & newRow) = Sheets("整體CSV").Range("B" & xStock)
Call 記錄各級別股東持股張數
End If
End If
Next
Range("A3:T" & newRow - 1).Sort Key1:=Range("A3"), Order1:=xlAscending, Header:=xlNo ' 排序 : 由小到大
End Sub
[ 程式解說 ] :
1. Sheets(“股權分散表資料清理”).Select : 切換至 股權分散表資料清理 工作表。
2. [A3:W3000].ClearContents : 保留第1~2列標題資料 / 清除股權分散表內所有舊資料(保留格式)。( PS. 採用自動化,必須記得將之前的舊資料清除乾淨,才不會造成數據錯誤。)
3. newRow = [A65536].End(xlUp).Offset(1).Row : 查找沒資料的新一列列號(Row)。
4. dataRow = Sheets(“整體CSV”).[A65536].End(xlUp).Row : 求得整體CSV工作表內最後一筆資料列號。
5. [A1] = Sheets(“整體CSV”).[A2] : A1欄 填進 日期 (股權分散表的列表日期)。
6. For xStock = 2 To dataRow Step 17 : 股權資料由第2列開始讀取,每遞增17列後,重新讀取,直至dataRow(最後一列)結束。
※ 意思是 : xStock(變數)從第2列開始讀取後,遞增至18列結束(其中17列),再從19列重新讀取,直到結束。( 參考下圖 — 橘色框線 )
( PS. Step : 是指 步(距離),也可想成是 : 間隔或跳轉,正數為遞增 / 負數為遞減。)
[ 圖解 ] :
7. xCode = Len(Sheets(“整體CSV”).Range(“B” & xStock)) : 目的是 — 查驗證券代號所含的字元數。利用 Len函數取得證券代號欄位中字串的長度,再將所得數值,傳回給 xCode(變數)。
[ 舉例 ] :
◎ xStock(變數)從第2列開始讀取( xStock = 2 ),Sheets(“整體CSV”).Range(“B” & xStock) => 讀取的欄位便是整體CSV工作表的B2欄位,其值為 : 815 (參考上圖),再帶進 Len(815),取得字串長度為 3,故 xCode = 3。
( PS. 第19列 — xCode = 2 / 55717列 — xCode = 6。)
8. 因為只保留二和四位數的股票資料,故利用 If xCode = 2 Then 和 ElseIf xCode = 4 Then 指令,來篩選出吻合的股票資料。
9. Range(“A” & newRow) = “00” & Sheets(“整體CSV”).Range(“B” & xStock) : 透過 If xCode = 2 Then 篩選出第19列吻合,其值為 : 50,但實際的台灣50股票代號是 : 0050,故在50前面,利用文字串接 “00”,來達成目的,再將0050存放於目前工作表(股權分散表資料清理)的最新空白列(newRow) A欄位置。
10. If IsNumeric(Sheets(“整體CSV”).Range(“B” & xStock)) Then : 透過 ElseIf xCode = 4 Then 篩選出吻合四位數的股票代號,卻發現有不吻合全數值的股票代號(如下圖 : YFE9),所以必須再做一次篩選,利用 IsNumeric函數,篩選出全數值的股票代號(如下圖 : 3854)。
[ 圖解 ] :
11. Call 記錄各級別股東持股張數 : 透過 If xCode = 2 Then 和 ElseIf xCode = 4 Then 指令,篩選出吻合的股票代號,便可透過 Call 函數,呼叫名為 : 記錄各級別股東持股張數 的 Sub 副程式(或稱 : 子程序)。
12. Range(“A3:T” & newRow – 1).Sort Key1:=Range(“A3”), Order1:=xlAscending, Header:=xlNo : 所有股票資料處理完成,做一次資料排序 : 由小到大。( PS. 不了解排序方法,可參考 : 資料排序。)
記錄各級別股東持股張數
※ 兩判別式篩選出股票代號後,都需要相同的程式碼,來記錄各級別股東的持股張數;此時只需要寫一個子程序(Sub),塞進共同的程式碼,再用 Call 來呼叫,便可分別記錄各級別股東持股張數。
[ 程式碼 / 記錄各級別股東持股張數 ] :
Sub 記錄各級別股東持股張數()
Dim xArray(16) As Variant, xNum As Integer
For xNum = 0 To 16
xData = Sheets("整體CSV").Range("E" & xStock + xNum)
If xData = 0 Then
xArray(xNum) = 0
Else
xArray(xNum) = xData / 1000
End If
Next
Range("B" & newRow).Resize(1, UBound(xArray)) = xArray ' 將陣列(xArray)內資料,寫成一列。
Range("S" & newRow) = Sheets("整體CSV").Range("D" & xStock + xNum - 1) ' 填進股東人數
Range("T" & newRow) = Range("R" & newRow) / Range("S" & newRow) ' 平均張數
newRow = [A65536].End(xlUp).Offset(1).Row
End Sub
[ 程式解說 ] :
1. Dim xArray(16) As Variant : 將 xArray(16)陣列(變數) 宣告為 萬用類型(Variant),可以存放各種不同類型的變數。陣列索引預設是從 0 開始,(16) 是指陣列的最後索引值。代表 xArray(16) 陣列內可以存放 17個變數值( 0、1、2 …… 16 )。
2. For xNum = 0 To 16 …… Next : 主要目的是要將各檔股票的各級別股東持股張數值,一一塞進 xArray 陣列裡。
[ 圖解 ] :
[ 程式解說 ] :
◎ For xNum = 0 To 16 : 利用迴圈,從 0 到 16,執行一遍。( PS. 抓取持股分級( 1 ~ 17 ),總共 : 17列。)
◎ xData = Sheets(“整體CSV”).Range(“E” & xStock + xNum) : 目的是要取得 E欄 — 各級別股東持股股數(參考上圖紫框),當開始抓取股票代號 : 0050,此時 xStock = 19 (參考上圖綠框),xNum = 0 (迴圈從 0 開始),所以 xData(變數值) 就對應到 E19 ( “E” & 19 + 0 ) = 102067287。
◎ If xData = 0 Then : 這組判斷式的用意是擔心 xData = 0。因為目前所得的資料為 : 股數,要將單位改成 : 張數,必須除以 1000,已知 0 / 1000 會產生錯誤,便多一道判別式來判別。
◎ xArray(xNum) = 0 和 xArray(xNum) = xData / 1000 : 藉由IF函數判別,分別將 E19 ~ E35 欄的資料,塞進 xArray(0) ~ (16)。
3. Range(“B” & newRow).Resize(1, UBound(xArray)) = xArray : 將 xArray(一維陣列變數)內資料,從新空白列(newRow) B欄 開始列表輸出,寫成一列。
☆ 陣列資料輸出到工作表,常常會使用 Range函數的 Resize 屬性,建立一個和陣列一樣大小的儲存格區域,用來寫入陣列資料。
( PS. Resize(1, UBound(xArray) — 其中 1 代表 : 列數 / UBound(xArray) : 傳回 xArray 的上標值 = 17,代表 : 行數。)
[ 圖解 ] :
4. Range(“S” & newRow) = Sheets(“整體CSV”).Range(“D” & xStock + xNum – 1) : 在 S欄位填進 整體CSV工作表/D欄(股東人數)資料。
( PS. xNum – 1 : 是因為 For xNum = 0 To 16 迴圈跑完後,其 xNum值 會等於 17,所以要將 xNum – 1,數據才不會發生錯誤。
5. Range(“T” & newRow) = Range(“R” & newRow) / Range(“S” & newRow) : R欄(集保張數) / S欄(股東人數) = T欄(平均張數/人)。
6. newRow = [A65536].End(xlUp).Offset(1).Row : 每一檔股票資料完成,便重新尋求新一列列號(Row)。
完整程式碼
[ 程式碼 / 股權分散表 ] :
Dim xStock As Long, newRow As Integer
Sub 下載_股權分散表CSV檔()
Sheets("股權分散表").Select
Range("B1").Hyperlinks(1).Follow ' 點擊 -- 執行B1欄超連結
Sheets("整體CSV").Select
Cells.Delete ' 刪除舊資料
' 下載檔案中CSV檔資料
Application.CutCopyMode = False
ActiveWorkbook.Queries.Add Name:="TDCC_OD_1-5", Formula:= _
"let" & Chr(13) & "" & Chr(10) & " 來源 = Csv.Document(File.Contents(""C:\Users\YANG\Downloads\TDCC_OD_1-5.csv""),[Delimiter="","", Columns=6, Encoding=65001, QuoteStyle=QuoteStyle.None])," & Chr(13) & "" & Chr(10) & " 已將標頭升階 = Table.PromoteHeaders(來源, [PromoteAllScalars=true])," & Chr(13) & "" & Chr(10) & " 已變更類型 = Table.TransformColumnTypes(已將標頭升階,{{""資料日期"", Int64.Type}, {""證券代號"", Int64.Type}, {""持股分級"", Int64.Type}, {""人數"", Int64.Type}," & _
" {""股數"", Int64.Type}, {""占集保庫存數比例%"", type number}})" & Chr(13) & "" & Chr(10) & "in" & Chr(13) & "" & Chr(10) & " 已變更類型" & _
""
' ActiveSheet.ListObjects("表格_外部資料_1").Name = "表格_TDCC_OD_1_5"
With ActiveSheet.ListObjects.Add(SourceType:=0, Source:= _
"OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=TDCC_OD_1-5;Extended Properties=""""" _
, Destination:=Range("$A$1")).QueryTable
.CommandType = xlCmdSql
.CommandText = Array("SELECT * FROM [TDCC_OD_1-5]")
.RowNumbers = False
.FillAdjacentFormulas = False
.PreserveFormatting = True
.RefreshOnFileOpen = False
.BackgroundQuery = True
.RefreshStyle = xlInsertDeleteCells
.SavePassword = False
.SaveData = True
.AdjustColumnWidth = True
.RefreshPeriod = 0
.PreserveColumnInfo = True
.ListObject.DisplayName = "表格_TDCC_OD_1_5"
.Refresh BackgroundQuery:=False
End With
ActiveWorkbook.Queries("TDCC_OD_1-5").Delete ' 刪除連線
Kill "C:\Users\YANG\Downloads\TDCC_OD_1-5*.csv" ' 刪除股權分散表CSV檔
Call 股權分散表資料清理
End Sub
' --------------------------------------------------------------------------------------------------------------------------------------------------
Sub 股權分散表資料清理()
Dim dataRow As Long, xCode As Integer
Sheets("股權分散表資料清理").Select
[A3:W3000].ClearContents ' 清除舊資料(保留格式)
newRow = [A65536].End(xlUp).Offset(1).Row
dataRow = Sheets("整體CSV").[A65536].End(xlUp).Row
[A1] = Sheets("整體CSV").[A2] ' 日期
For xStock = 2 To dataRow Step 17
xCode = Len(Sheets("整體CSV").Range("B" & xStock)) ' 查驗代號字元數
If xCode = 2 Then
Range("A" & newRow) = "00" & Sheets("整體CSV").Range("B" & xStock)
Call 記錄各級別股東持股張數
ElseIf xCode = 4 Then
If IsNumeric(Sheets("整體CSV").Range("B" & xStock)) Then
Range("A" & newRow) = Sheets("整體CSV").Range("B" & xStock)
Call 記錄各級別股東持股張數
End If
End If
Next
Range("A3:T" & newRow - 1).Sort Key1:=Range("A3"), Order1:=xlAscending, Header:=xlNo ' 排序 : 由小到大
End Sub
' --------------------------------------------------------------------------------------------------------------------------------------------------
Sub 記錄各級別股東持股張數()
Dim xArray(17) As Variant, xNum As Integer
For xNum = 0 To 16
xData = Sheets("整體CSV").Range("E" & xStock + xNum)
If xData = 0 Then
xArray(xNum) = 0
Else
xArray(xNum) = xData / 1000
End If
Next
Range("B" & newRow).Resize(1, UBound(xArray)) = xArray ' 將陣列(xArray)內資料,寫成一列。
Range("S" & newRow) = Sheets("整體CSV").Range("D" & xStock + xNum - 1) ' 填進股東人數
Range("T" & newRow) = Range("R" & newRow) / Range("S" & newRow) ' 平均張數
newRow = [A65536].End(xlUp).Offset(1).Row
End Sub
[ 注意 ] :
※ 這組程式採用結構化設計,利用 Call / Sub 來區分結構,讓程式更為鮮明,但必須注意的是 : 變數宣告。
◎ 一般在 Sub 裡面設定的變數,屬 : 區域變數 — 影響範圍僅限於自己所屬的 Sub (子程序)。
◎ 若是要設計 跨Sub 的變數,如 : 程式中的 xStock / newRow 變數,需要在 Sub 股權分散表資料清理 及 Sub 記錄各級別股東持股張數 中運作計算,此時就要將變數宣告,改成 : 模組層級變數 — 宣告位置必須在第一個Sub程序的上方。
實作圖 / Excel 股權分散表
[ 實作圖 ] :
結論
※ 股權分散表 是用於了解上市公司的股權持有結構。透過VBA程式全面分析股權分散表,可以揭示公司股東的分佈情況、股權集中度和持股變化,投資者可以從中瞭解公司的所有權結構並洞察其潛在風險,進而做出更明智的投資決策,提升投資成功的機會。
V B A 資 源