iT邦幫忙

2022 iThome 鐵人賽

DAY 14
2
Software Development

從 Node.js 開發者到量化交易者:打造屬於自己的投資系統系列 第 14

Day 14 - 市場資金去哪裡:產業資金流向

  • 分享至 

  • xImage
  •  

前一天我們介紹如何運用 產業分類股價指數 並透過 相對強度分析,找出表現強勁以及比較弱勢的產業,而除了產業分類股價指數之外,觀察 產業資金流向 也可以幫助投資人找出市場目前的主流族群。因為臺股是一個比較淺碟的市場,市場資金匯集的地方,通常就是目前盤面的主流,而跟隨市場的熱點題材,就有機會搭上主力資金的轎子,就如同小米創辦人雷軍曾說的:「站在風口上,連豬都會飛」。

從資金流向找出市場主流

產業分析是一門專業,我們可能沒辦法像分析師一樣,瞭解產業的優勢、劣勢、機會與威脅,甚至預測產業未來發展的前景。不過行情是由真金白銀堆積起來的,觀察市場的資金流向,我們可以找出哪些產業的股票正受到市場資金的青睞或大戶主力的注目。《投資人財經日報》(Investor's Daily)創辦人 威廉‧歐尼爾(William O'Neil)說:「股市贏家的法則是,不買落後股,不買平庸股,全心全意鎖定領導股。」從這些產業的股票進一步篩選,就有機會找出合適的投資標的。

證交所在每日盤後會公布「各類指數日成交量值」;櫃買中心在交易盤後也會揭露「類股成交價量比重」。每天記錄上市櫃各產業成交比重的變化,我們就可以知道資金流入哪些產業,或者資金撤出哪些產業。威廉‧歐尼爾 曾說:「主流類中的股票,常能漲得驚天動地,但其他平庸個股,連一絲漣漪都不會起。」如果發現資金流入的產業,表現明顯比大盤強勁,我們就知道這些產業的個股是目前市場上矚目的焦點,從這些資金集中的產業類股進行篩選,就有比較高的機會找出產業領導股及表現強勢的股票。

2021 年,臺股大航海時代

從 2020 年 COVID-19 疫情爆發造成貨物供應鏈的中斷,到疫情趨緩因經濟復甦引發突然的爆量需求,造成全球貨櫃大塞港,也使得海運運價大幅提升,讓航運股直接受惠。事實上,往年的航運類股並不是市場矚目的焦點,成交比重幾乎在 1% 上下,但當市場有題材點火,就會吸引資金買盤進場。2021 年航運股的熱潮,特別是貨櫃航運的成交量,幾乎全年處於高檔。

月份 成交比重 月份 成交比重
01 7.28% 07 38.13%
02 6.83% 08 30.46%
03 7.38% 09 19.39%
04 13.23% 10 18.37%
05 24.48% 11 18.61%
06 35.14% 12 19.52%

Source:臺灣證券交易所

根據期交所的產業分類比重統計,在 2020 年底,航運類股的市值在集中市場僅佔 1.7504%。然而在 2021 年 6 至 8 月,航運類股在集中市場卻可以有高達 30% 以上的成交比重,可以說是相當瘋狂!市場成交重心尤其集中在貨櫃三雄:長榮(2603)、陽明(2609)、萬海(2615),這三檔股票在該時期經常是成交金額榜上的前三名。

https://ithelp.ithome.com.tw/upload/images/20220914/20150150UZQbca6ChN.png

Source:臺灣證券交易所

上圖是航運類指數在 2021 年的價格走勢及市場成交比重對照。2021 年上半年隨著航運類股的成交比重逐月上升,航運類指數也在 2021 年 7 月初的時候來到歷史高點,不過行情總有曲終人散之時,當該產業的資金熱潮退卻之時,該產業表現也明顯走弱。為了讓資金有更好的使用效率,這時不妨可以選擇其他表現更強勁的產業類股,畢竟市場上總有派對。

查詢集中市場資金流向

在證交所網站的 各類指數日成交量值 頁面,可以按日查詢集中市場各類指數日成交量值。

證交所首頁 > 交易資訊 > 盤後資訊 > 各類指數日成交量值

在「各類指數日成交量值」頁面選取「資料日期」並按下「查詢」後,就會列出該日各類指數日成交量值。

https://ithelp.ithome.com.tw/upload/images/20220914/20150150XZ6Xf9u9Mr.png

點擊「列印 / HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「民國 111 年 07 月 01 日」,我們會得到以下 URL:

https://www.twse.com.tw/exchangeReport/BFIAMU?response=html&date=20220701

以上 URL 可設定的參數如下:

  • response:回應資料的格式。指定 html 輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。
  • date:資料日期。接受的日期格式為 yyyyMMdd,如 20220701

我們將 URL 查詢參數改為 response=json&date=20220701,證交所就會以 JSON 格式資料回應 2022 年 7 月 1 日的各類指數日成交量值:

{
  "stat": "OK",
  "date": "20220701",
  "title": "111年07月01日各類指數日成交量值",
  "fields": [
    "分類指數名稱",
    "成交股數",
    "成交金額",
    "成交筆數",
    "漲跌指數"
  ],
  "data": [
    [
      "水泥類指數          ",
      "38,877,971",
      "1,562,316,025",
      "17,749",
      "2.17"
    ],
    [
      "食品類指數          ",
      "24,443,913",
      "1,235,598,472",
      "16,131",
      "2.20"
    ],
    [
      "塑膠類指數          ",
      "81,410,198",
      "4,021,920,818",
      "33,509",
      "-2.60"
    ],
    ......
  ],
  "notes": [
    "以上統計資料包括:一般交易、鉅額交易、零股交易及盤後定價交易之量值。"
  ],
  "subtitle": []
}

實作:取得集中市場資金流向

由於證交所的回應資料沒有包含指數代號,因此我們需要先實作將指數名稱轉換成指數代號的工具函式。請在 libs/common/src 目錄下建立 utils 資料夾,並新增 get-twse-index-symbol-by-name.util.ts 檔案,實作 getTwseIndexSymbolByName() 工具函式將指數名稱轉換成指數代號:

import { Index } from '../enums/index.enum';

export function getTwseIndexSymbolByName(name: string) {
  const indices = {
    '水泥類指數': Index.Cement,
    '食品類指數': Index.Food,
    '塑膠類指數': Index.Plastic  ,
    '紡織纖維類指數': Index.Textiles,
    '電機機械類指數': Index.ElectricMachinery,
    '電器電纜類指數': Index.ElectricalAndCable,
    '化學生技醫療類指數': Index.ChemicalBiotechnologyAndMedicalCare,
    '化學類指數': Index.Chemical,
    '生技醫療類指數': Index.BiotechnologyAndMedicalCare,
    '玻璃陶瓷類指數': Index.GlassAndCeramic,
    '造紙類指數': Index.PaperAndPulp,
    '鋼鐵類指數': Index.IronAndSteel,
    '橡膠類指數': Index.Rubber,
    '汽車類指數': Index.Automobile,
    '電子類指數': Index.Electronics,
    '半導體類指數': Index.Semiconductors,
    '電腦及週邊設備類指數': Index.ComputerAndPeripheralEquipment,
    '光電類指數': Index.Optoelectronics,
    '通信網路類指數': Index.CommunicationsTechnologyAndInternet,
    '電子零組件類指數': Index.ElectronicPartsComponents,
    '電子通路類指數': Index.ElectronicProductsDistirbution,
    '資訊服務類指數': Index.InformationService,
    '其他電子類指數': Index.OtherElectronics,
    '建材營造類指數': Index.BuildingMaterialsAndConstruction,
    '航運業類指數': Index.ShippingAndTransportation,
    '觀光事業類指數': Index.Tourism,
    '金融保險類指數': Index.FinancialAndInsurance,
    '貿易百貨類指數': Index.TradingAndConsumerGoods,
    '油電燃氣類指數': Index.OilGasAndElectricity,
    '其他類指數': Index.Other,
    '電子工業類指數': Index.Electronics,
    '航運類指數': Index.ShippingAndTransportation,
    '觀光類指數': Index.Tourism,
  };
  return indices[name];
}

getTwseIndexSymbolByName() 函式中,我們用到先前已經定義的 Index 列舉型別,代表指數代號。完成後,在 libs/common/src/utils 目錄下新增 index.ts 檔案,將 get-twse-index-symbol-by-name.util.ts 匯出:

export * from './get-twse-index-symbol-by-name.util';

然後開啟 libs/common/src/index.ts 檔案,加入:

export * from './utils';

完成後,我們之後就可以在 Nest 應用程式透過以下方式引用該工具函式:

import { getTwseIndexSymbolByName } from '@speculator/common';

接著我們要實作取得集中市場資金流向。開啟 src/scraper/twse-scraper.service.ts 檔案,在 TwseScraperService 實作 fetchIndicesTrades() 方法,取得集中市場各類指數日成交量值:

import * as _ from 'lodash';
import * as cheerio from 'cheerio';
import * as iconv from 'iconv-lite';
import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { firstValueFrom } from 'rxjs';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { getTwseIndexSymbolByName } from '@speculator/common';

@Injectable()
export class TwseScraperService {
  constructor(private httpService: HttpService) {}

  ...

  async fetchIndicesTrades(date: string) {
    // // 將 `date` 轉換成 `yyyyMMdd` 格式
    const formattedDate = DateTime.fromISO(date).toFormat('yyyyMMdd');

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      response: 'json',     // 指定回應格式為 JSON
      date: formattedDate,  // 指定資料日期
    });
    const url = `https://www.twse.com.tw/exchangeReport/BFIAMU?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.stat === 'OK') ? response.data : null);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 取得市場成交量值
    const market = await this.fetchMarketTrades(date);

    // 計算成交比重
    const data = responseData.data.map(row => {
      const [
        name,         // 分類指數名稱
        tradeVolume,  // 成交股數
        tradeValue,   // 成交金額
        transaction,  // 成交筆數
        change,       // 漲跌指數
      ] = row;
      return {
        date,
        symbol: getTwseIndexSymbolByName(name.trim()),
        tradeVolume: numeral(tradeVolume).value(),
        tradeValue: numeral(tradeValue).value(),
        tradeWeight: +numeral(tradeValue).divide(market.tradeValue).multiply(100).format('0.00'),
      };
    });

    return data;
  }
}

這裡引用到了我們剛剛完成的 getTwseIndexSymbolByName() 工具函式。在 fetchIndicesTrades() 方法中,需要指定 date 參數,表示要取得集中市場產業資金流向的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一個產業指數的物件,物件欄位包含如下:

  • date:日期
  • symbol:指數代號
  • tradeVolume:成交股數
  • tradeValue:成交金額
  • tradeWeight:成交比重

完成後,我們只要呼叫 TwseScraperServicefetchIndicesTrades() 方法,就可以按日期取得集中市場產業資金流向。以日期 2022-07-01 為例:

[
  {
    date: '2022-07-01',
    symbol: 'IX0010',
    tradeVolume: 38877971,
    tradeValue: 1562316025,
    tradeWeight: 0.51
  },
  {
    date: '2022-07-01',
    symbol: 'IX0011',
    tradeVolume: 24443913,
    tradeValue: 1235598472,
    tradeWeight: 0.4
  },
  {
    date: '2022-07-01',
    symbol: 'IX0012',
    tradeVolume: 81410198,
    tradeValue: 4021920818,
    tradeWeight: 1.3
  },
  ......
]

查詢櫃買市場資金流向

在櫃買中心網站的 類股成交價量比重 頁面,可以按日查詢類股成交價量比重。

櫃買中心首頁 > 上櫃 > 歷史熱門資料 > 類股成交價量比重

在「類股成交價量比重」頁面選取「資料日期」後,就會列出該日類股成交價量比重。

https://ithelp.ithome.com.tw/upload/images/20220914/20150150RaEYUwLkWZ.png

點擊「列印/匯出HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「111/07/01」,我們會得到以下 URL:

https://www.tpex.org.tw/web/stock/historical/trading_vol_ratio/sectr_result.php?l=zh-tw&d=111/07/01&s=undefined&o=htm

以上 URL 可設定的參數如下:

  • l:輸出資料的語系。zh-tw 為正體中文;en-us 為英文。
  • d:資料日期。接受 民國年/月/日 的日期格式。需要注意,若 l 參數指定為 en-us,則 d 參數需改成 西元年/月/日 的日期格式。
  • s:指定欄位依照升冪或降冪排序。例如 1,asc 是按成交金額(元)升冪排序;2,desc 則按成交比重(%)降冪,依此類推。
  • o:資料輸出的格式。指定 htm 表示輸出 HTML 文件;改為 csv 可以另存 CSV 檔案;設定成 json 或不指定則回應 JSON 格式資料。

我們將 URL 查詢參數改為 l=zh-tw&o=json&d=111/07/01,櫃買中心就會以 JSON 格式資料回應 2022 年 7 月 1 日的櫃買市場類股成交價量比重:

{
  "reportDate": "111/07/22",
  "iTotalRecords": 26,
  "aaData": [
    [
      "食品工業",
      "21,618,648",
      "0.03",
      "1,152,044",
      "0.14"
    ],
    [
      "塑膠工業",
      "24,769,974",
      "0.04",
      "1,194,769",
      "0.14"
    ],
    [
      "紡織纖維",
      "42,675,258",
      "0.06",
      "2,931,026",
      "0.35"
    ],
    ......
  ]
}

實作:取得櫃買市場資金流向

由於櫃買中心的回應資料沒有包含指數代號,因此我們需要先實作將指數名稱轉換成指數代號的工具函式。請在 libs/common/src 目錄下建立 utils 資料夾,並新增 get-tpex-index-symbol-by-name.util.ts 檔案,實作 getTpexIndexSymbolByName() 工具函式將指數名稱轉換成指數代號:

import { Index } from '../enums/index.enum';

export function getTpexIndexSymbolByName(name: string) {
  // 無對應指數: 塑膠工業, 橡膠工業, 油電燃氣業, 貿易百貨, 貿易百貨, 金融業, 電器電纜, 電子商務, 食品工業
  const indices = {
    '光電業': Index.TPExOptoelectronic,
    '其他': Index.TPExOther,
    '其他電子業': Index.TPExOtherElectronic,
    '化學工業': Index.TPExChemical,
    '半導體業': Index.TPExSemiconductors,
    '建材營造': Index.TPExBuildingMaterialsAndConstruction,
    '文化創意業': Index.TPExCulturalAndCreative,
    '生技醫療': Index.TPExBiotechnologyAndMedicalCare,
    '紡織纖維': Index.TPExTextiles,
    '航運業': Index.TPExShippingAndTransportation,
    '觀光事業': Index.TPExTourism,
    '資訊服務業': Index.TPExInformationService,
    '通信網路業': Index.TPExCommunicationsAndInternet,
    '鋼鐵工業': Index.TPExIronAndSteel,
    '電子通路業': Index.TPExElectronicProductsDistribution,
    '電子零組件業': Index.TPExElectronicPartsComponents,
    '電機機械': Index.TPExElectricMachinery,
    '電腦及週邊設備業': Index.TPExComputerAndPeripheralEquipment,
  };
  return indices[name];
}

getTpexIndexSymbolByName() 函式中,我們用到先前已經定義的 Index 列舉型別,代表指數代號。在櫃買市場中,不是所有產業類別都有對應的產業指數,這邊我們就暫且忽略。完成後,開啟 libs/common/src/utils/index.ts 檔案,將 get-tpex-index-symbol-by-name.util.ts 匯出:

export * from './get-tpex-index-symbol-by-name.util';

完成後,我們之後就可以在 Nest 應用程式透過以下方式引用該工具函式:

import { getTpexIndexSymbolByName } from '@speculator/common';

接著我們要實作取得櫃買市場資金流向。開啟 src/scraper/tpex-scraper.service.ts 檔案,在 TpexScraperService 實作 fetchIndicesTrades() 方法,取得櫃買市場類股成交價量比重:

import * as _ from 'lodash';
import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { firstValueFrom } from 'rxjs';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Index, getTpexIndexSymbolByName } from '@speculator/common';

@Injectable()
export class TpexScraperService {
  constructor(private httpService: HttpService) {}

  ...

  async fetchIndicesTrades(date: string) {
    // `date` 轉換成 `民國年/MM/dd` 格式
    const dt = DateTime.fromISO(date);
    const year = dt.get('year') - 1911;
    const formattedDate = `${year}/${dt.toFormat('MM/dd')}`;

    // 建立 URL 查詢參數
    const query = new URLSearchParams({
      l: 'zh-tw',       // 指定語系為正體中文
      d: formattedDate, // 指定資料日期
      o: 'json',        // 指定回應格式為 JSON
    });
    const url = `https://www.tpex.org.tw/web/stock/historical/trading_vol_ratio/sectr_result.php?${query}`;

    // 取得回應資料
    const responseData = await firstValueFrom(this.httpService.get(url))
      .then(response => (response.data.iTotalRecords > 0) ? response.data : null);

    // 若該日期非交易日或尚無成交資訊則回傳 null
    if (!responseData) return null;

    // 整理回應資料
    const indices = responseData.aaData.map(row => {
      const [
        name,               // 類股名稱
        tradeValue,         // 成交金額(元)
        tradeValueWeight,   // 成交金額比重(%)
        tradeVolume,        // 成交股數
        tradeVolumeWeight,  // 成交股數比重(%)
      ] = row;
      return {
        date,
        symbol: getTpexIndexSymbolByName(name),
        tradeVolume: numeral(tradeVolume).value(),
        tradeValue: numeral(tradeValue).value(),
        tradeWeight: numeral(tradeValueWeight).value(),
      };
    });

    // 計算電子工業成交量值
    const electronic = indices.reduce((trades, data) => {
      return [
        Index.TPExSemiconductors,
        Index.TPExComputerAndPeripheralEquipment,
        Index.TPExOptoelectronic,
        Index.TPExCommunicationsAndInternet,
        Index.TPExElectronicPartsComponents,
        Index.TPExElectronicProductsDistribution,
        Index.TPExInformationService,
        Index.TPExOtherElectronic,
      ].includes(data.symbol)
        ? { ...trades,
          tradeVolume: trades.tradeVolume + data.tradeVolume,
          tradeValue: trades.tradeValue + data.tradeValue,
          tradeWeight: trades.tradeWeight + data.tradeWeight,
        } : trades;
    }, { date, symbol: Index.TPExElectronic, tradeVolume: 0, tradeValue: 0, tradeWeight: 0 });

    indices.push(electronic);

    // 過濾無對應指數的產業別
    const data = indices.filter(index => index.symbol);

    return data;
  }
}

這裡引用到了我們剛剛完成的 getTpexIndexSymbolByName() 工具函式。在 fetchIndicesTrades() 方法中,需要指定 date 參數,表示要取得櫃買市場產業資金流向的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一個產業指數的物件,物件欄位包含如下:

  • date:日期
  • symbol:指數代號
  • tradeVolume:成交股數
  • tradeValue:成交金額
  • tradeWeight:成交比重

完成後,我們只要呼叫 TpexScraperServicefetchIndicesTrades() 方法,就可以按日期取得櫃買市場產業資金流向。以日期 2022-07-01 為例:

[
  {
    date: '2022-07-01',
    symbol: 'IX0055',
    tradeVolume: 67195481,
    tradeValue: 7840393278,
    tradeWeight: 11.59
  },
  {
    date: '2022-07-01',
    symbol: 'IX0100',
    tradeVolume: 45417430,
    tradeValue: 2210770490,
    tradeWeight: 3.27
  },
  {
    date: '2022-07-01',
    symbol: 'IX0099',
    tradeVolume: 29686769,
    tradeValue: 2214351262,
    tradeWeight: 3.27
  },
  ......
]

本日小結

  • 臺股是比較淺碟的市場,市場資金匯集的地方,通常是目前盤面主流。
  • 觀察市場資金流向,可以找出哪些產業正受到市場資金的青睞。
  • 當一個產業的資金退潮,可以適時轉往其他表現更強勁的產業。
  • 瞭解如何在證交所網站上查詢並實作取得集中市場資金流向的方法。
  • 瞭解如何在櫃買中心網站上查詢並實作取得櫃買市場資金流向的方法。

Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰
本系列文已正式出版為《Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰》。本書新增了全新內容和實用範例,為你提供更深入的學習體驗!歡迎參考選購,開始你的量化投資之旅!
天瓏網路書店連結:https://www.tenlong.com.tw/products/9786263336070


上一篇
Day 13 - 觀察產業輪動:產業分類股價指數
下一篇
Day 15 - 技術分析之母:股價K線
系列文
從 Node.js 開發者到量化交易者:打造屬於自己的投資系統31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言