由上而下投資法的 第一階段 是評估 大盤,所以前面我們花了比較長的篇幅,介紹如何運用交易所的公開數據解讀大盤籌碼,乃至於總體經濟,目的是評估當下的大環境狀況是否適合投資股票,或依據個人可承擔風險的程度,調整適當的曝險部位與持股比例。第二階段 是評估哪個 產業 的表現相對強勁。景氣會循環,產業也會輪動,因此不同階段下的產業表現也會有所差異,所以在買進股票時,也應該考慮股票所屬產業的情況。想要瞭解目前哪個產業強勢,哪個產業弱勢,最直接的方法,就是觀察集中市場和櫃買市場的「產業分類股價指數」。
股票市場是由各種產業類別的股票所組成。在臺灣集中市場,除了衡量市場整體的 發行量加權股價指數 外,也依產業類別劃分成 28 種 產業分類股價指數。期交所每個月底會公布最新的 臺灣證券交易所發行量加權股價指數成分股暨市值比重,以 2022 年 8 月份的資料為例,集中市場的產業分類及其比重如下:
產業別 | 比重 | 產業別 | 比重 |
---|---|---|---|
半導體業 | 37.4544% | 電機機械 | 1.4796% |
金融保險 | 12.0924% | 建材營造 | 1.3758% |
電子零組件業 | 5.6487% | 貿易百貨 | 1.2676% |
其他電子業 | 4.7896% | 紡織纖維 | 1.247% |
電腦及週邊設備業 | 4.58% | 水泥工業 | 1.0009% |
通信網路業 | 4.3608% | 生技醫療業 | 0.8674% |
塑膠工業 | 3.6858% | 電子通路業 | 0.8598% |
其他 | 3.6358% | 化學工業 | 0.8537% |
航運業 | 3.5056% | 橡膠工業 | 0.6031% |
光電業 | 2.4388% | 電器電纜 | 0.4987% |
鋼鐵工業 | 1.8909% | 造紙工業 | 0.3038% |
油電燃氣業 | 1.8786% | 觀光事業 | 0.2945% |
食品工業 | 1.549% | 資訊服務業 | 0.1643% |
汽車工業 | 1.5363% | 玻璃陶瓷 | 0.1371% |
Source:臺灣期貨交易所
除了以上 28 種產業分類股價指數外,證交所還編制以下涵蓋兩種以上產業類別的產業分類股價指數:
看到上述產業指數時,需要注意它們包含了兩種以上的產業類別,而不是指單一產業。
在臺灣櫃買市場,除了 櫃買指數外,櫃買中心也編制了 18 種 產業分類股價指數。期交所在每個月底也會更新 證券櫃檯買賣中心發行量加權股價指數成份股比重,以 2022 年 8 月份的資料為例,產業分類及其比重如下:
產業別 | 比重 | 產業別 | 比重 |
---|---|---|---|
半導體業 | 28.7893% | 文化創意業 | 2.424% |
生技醫療業 | 18.2107% | 鋼鐵工業 | 1.9812% |
光電業 | 8.7755% | 建材營造業 | 1.906% |
電子零組件業 | 7.1648% | 資訊服務業 | 1.2657% |
電腦及週邊設備業 | 4.5378% | 觀光事業 | 1.0337% |
其他電子業 | 4.1929% | 化學工業 | 0.8351% |
其他 | 3.7207% | 電子通路業 | 0.7078% |
通信網路業 | 3.6034% | 航運業 | 0.4462% |
電機機械 | 2.8843% | 紡織纖維 | 0.3173% |
Source:臺灣期貨交易所
櫃買市場涵蓋兩種以上產業類別的產業分類股價指數僅有電子類指數:
剖析了臺股集中市場與櫃買市場的產業分類比重後,我們可以發現 電子類股 在臺灣股市就佔了 六成 的市值比重,因此電子股表現的好壞,會牽動大盤整體的走勢。
相對強度(Relative Strength)簡稱 RS,是用來比較某種資產相對於另一種資產的表現,可以運用於任何兩種資產。將某種資產的價格序列,除以另一種資產對應的價格序列,就能繪製成 RS 線,並且可以和價格走勢圖做比較。
這裡的 RS(Relative Strength)指的是「相對強度」,與「相對強度指標」 RSI(Relative Strength Index)是完全不同的東西,千萬不要搞混了。
相對強度的概念可以用來比較產業類股與大盤之間的表現。假設我們要比較某一產業類股與大盤的相對強度,計算方式為:
相對強度 (RS) = 產業分類股價指數 / 發行量加權股價指數
只要我們取得一段期間大盤指數與產業分類股價指數,就可以計算出這段期間產業類股的相對強度並繪製出 RS 線。當 RS 線上升,代表這段期間該類股的表現比大盤強勢;當 RS 線下降,代表這段期間該類股的表現比大盤弱勢。下圖是 2022 年上半年的集中市場產業熱力圖:
Source:富果產業熱力圖
2022 年上半年很明顯是個空頭市場,這段期間加權指數的跌幅是 -20.58%,雖然整體市場是下跌的,不過仍然有正報酬的產業類股。我們以表現比大盤強勁的 生技醫療類 和表現弱於大盤的 半導體類 為例,分別計算該產業在 2022 年上半年的相對強度:
Source:臺灣證券交易所
從相對強度的走勢圖可以看出,在 2022 年上半年,生技醫療類指數的 RS 在大部分時間是上升趨勢,而半導體類指數的 RS 大部分時間則是下降趨勢,這也導致了這兩個產業在這段期間有截然不同的表現。
透過相對強度分析,可以幫助我們找出相對強勁的產業,避免選擇表現相對疲弱的產業。因此,如果想要取得優於大盤的績效,最好是選擇 RS 曲線向上或開始向上翻升的類股,並且避開 RS 曲線向下反轉或持續下降的類股。
每檔上市櫃股票都有股票代號,指數也不例外。為了方便整理及儲存資料,我們先取得所有產業分類股價指數的代號,並且將這些指數代號及名稱對照以列舉(Enum)的形式儲存下來。
在證交所網站,可以找到 本國指數國際證券辨識號碼一覽表:
證交所首頁 > 產品與服務 > 證券編碼 > 證券編碼公告
在「本國指數國際證券辨識號碼一覽表」頁面,列出了各式各樣的指數,我們只需要整理出 加權指數、櫃買指數 以及 上市櫃產業分類股價指數 的代號即可。
為了方便之後我們在不同的 Nest 應用程式之間共享程式碼,我們使用 Nest CLI 建立一個名為 common
函式庫 (library):
$ nest g lib common
執行命令後,Nest CLI 會提示需要指定一個前綴 (prefix) 名稱,預設使用 @app
。選一個你想要的名稱即可,這裡我們使用 @speculator
為前綴。
? What prefix would you like to use for the library (default: @app)? @speculator
執行命令完成後,Nest CLI 會在專案下新增 libs/common
目錄。同時,專案目錄下的 nest-cli.json
檔案,我們也可以發現在 projects
屬性下已經新增了一個 common
lib 的設定:
:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"projects": {
"common": {
"type": "library",
"root": "libs/common",
"entryFile": "index",
"sourceRoot": "libs/common/src",
"compilerOptions": {
"tsConfigPath": "libs/common/tsconfig.lib.json"
}
}
},
"compilerOptions": {
"webpack": true
}
}
建立 @speculator/common
lib 後,Nest CLI 預設會在 libs/common/src
目錄下建立 common.module.ts
、common.service.ts
與 common.service.spec.ts
檔案,由於我們並不會使用到它們,可將其刪除。然後,在 libs/common/src
目錄下建立 enums
資料夾,並新增 index.enum.ts
檔案,定義一系列的指數代碼:
export enum Index {
TAIEX = 'IX0001', // 發行量加權股價指數
NonFinance = 'IX0007', // 未含金融指數
NonElectronics = 'IX0008', // 未含電子指數
NonFinanceNonElectronics = 'IX0009', // 未含金融電子指數
Cement = 'IX0010', // 水泥類指數
Food = 'IX0011', // 食品類指數
Plastic = 'IX0012', // 塑膠類指數
CementAndCeramic = 'IX0013', // 水泥窯製類指數
PlasticAndChemical = 'IX0014', // 塑膠化工類指數
Electrical = 'IX0015', // 機電類指數
Textiles = 'IX0016', // 紡織纖維類指數
ElectricMachinery = 'IX0017', // 電機機械類指數
ElectricalAndCable = 'IX0018', // 電器電纜類指數
ChemicalBiotechnologyAndMedicalCare = 'IX0019', // 化學生技醫療類指數
Chemical = 'IX0020', // 化學類指數
BiotechnologyAndMedicalCare = 'IX0021', // 生技醫療類指數
GlassAndCeramic = 'IX0022', // 玻璃陶瓷類指數
PaperAndPulp = 'IX0023', // 造紙類指數
IronAndSteel = 'IX0024', // 鋼鐵類指數
Rubber = 'IX0025', // 橡膠類指數
Automobile = 'IX0026', // 汽車類指數
Electronics = 'IX0027', // 電子工業類指數
Semiconductors = 'IX0028', // 半導體類指數
ComputerAndPeripheralEquipment = 'IX0029', // 電腦及週邊設備類指數
Optoelectronics = 'IX0030', // 光電類指數
CommunicationsTechnologyAndInternet = 'IX0031', // 通信網路類指數
ElectronicPartsComponents = 'IX0032', // 電子零組件類指數
ElectronicProductsDistirbution = 'IX0033', // 電子通路類指數
InformationService = 'IX0034', // 資訊服務類指數
OtherElectronics = 'IX0035', // 其他電子類指數
BuildingMaterialsAndConstruction = 'IX0036', // 建材營造類指數
ShippingAndTransportation = 'IX0037', // 航運類指數
Tourism = 'IX0038', // 觀光事業類指數
FinancialAndInsurance = 'IX0039', // 金融保險類指數
TradingAndConsumerGoods = 'IX0040', // 貿易百貨類指數
OilGasAndElectricity = 'IX0041', // 油電燃氣類指數
Other = 'IX0042', // 其他類指數
TPEX = 'IX0043', // 櫃檯指數
TPExTextiles = 'IX0044', // 櫃檯紡纖類指數
TPExElectricMachinery = 'IX0045', // 櫃檯機械類指數
TPExIronAndSteel = 'IX0046', // 櫃檯鋼鐵類指數
TPExElectronic = 'IX0047', // 櫃檯電子類指數
TPExBuildingMaterialsAndConstruction = 'IX0048', // 櫃檯營建類指數
TPExShippingAndTransportation = 'IX0049', // 櫃檯航運類指數
TPExTourism = 'IX0050', // 櫃檯觀光類指數
TPExChemical = 'IX0051', // 櫃檯化工類指數
TPExBiotechnologyAndMedicalCare = 'IX0052', // 櫃檯生技醫療類指數
TPExSemiconductors = 'IX0053', // 櫃檯半導體類指數
TPExComputerAndPeripheralEquipment = 'IX0054', // 櫃檯電腦及週邊類指數
TPExOptoelectronic = 'IX0055', // 櫃檯光電業類指數
TPExCommunicationsAndInternet = 'IX0056', // 櫃檯通信網路類指數
TPExElectronicPartsComponents = 'IX0057', // 櫃檯電子零組件類指數
TPExElectronicProductsDistribution = 'IX0058', // 櫃檯電子通路類指數
TPExInformationService = 'IX0059', // 櫃檯資訊服務類指數
TPExCulturalAndCreative = 'IX0075', // 櫃檯文化創意業類指數
TPExOtherElectronic = 'IX0099', // 櫃檯其他電子類指數
TPExOther = 'IX0100', // 櫃檯其他類指數
}
定義指數清單後,在 libs/common/src/enums
目錄下新增 index.ts
檔案,將 index.enum.ts
匯出:
export * from './index.enum';
再修改 libs/common/src/index.ts
檔案
export * from './enums';
完成後,我們之後就可以在 Nest 應用程式透過以下方式引用這份檔案:
import { Index } from '@speculator/common';
在交易日盤中,集中市場每 5 秒會更新一次指數行情,並於盤後會公布數據。
在證交所網站的 每 5 秒指數盤後統計 頁面,可以按日查詢加權指數及產業分類股價指數行情。
證交所首頁 > 指數資訊 > TWSE自行編製指數 > 每5秒指數盤後統計
在「每5秒指數盤後統計」頁面選取「資料日期」並按下「查詢」後,就會列出該日每 5 秒指數盤後統計。
這裡需要注意,時間 09:00:00
列出的是前次交易日的指數收盤數字;時間 09:00:05
才是當日的指數開盤數字。
點擊「列印 / HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「民國 111 年 07 月 01 日」,我們會得到以下 URL:
https://www.twse.com.tw/exchangeReport/MI_5MINS_INDEX?response=html&date=20220701
以上 URL 可設定的參數如下:
response
:回應資料的格式。指定 html
輸出 HTML 文件;改為 csv
可以另存 CSV 檔案;設定成 json
或不指定則回應 JSON 格式資料。date
:資料日期。接受的日期格式為 yyyyMMdd
,如 20220701
。我們將 URL 查詢參數改為 response=json&date=20220701
,證交所就會以 JSON 格式資料回應 2022 年 7 月 1 日的每 5 秒指數盤後統計:
{
"stat": "OK",
"date": "20220701",
"title": "111年07月01日每5秒指數統計",
"fields": [
"時間",
"發行量加權股價指數",
"未含金融保險股指數",
"未含電子股指數",
"未含金融電子股指數",
"水泥類指數",
"食品類指數",
"塑膠類指數",
"紡織纖維類指數",
"電機機械類指數",
"電器電纜類指數",
"化學生技醫療類指數",
"化學類指數",
"生技醫療類指數",
"玻璃陶瓷類指數",
"造紙類指數",
"鋼鐵類指數",
"橡膠類指數",
"汽車類指數",
"電子類指數",
"半導體類指數",
"電腦及週邊設備類指數",
"光電類指數",
"通信網路類指數",
"電子零組件類指數",
"電子通路類指數",
"資訊服務類指數",
"其他電子類指數",
"建材營造類指數",
"航運類指數",
"觀光類指數",
"金融保險類指數",
"貿易百貨類指數",
"油電燃氣類指數",
"其他類指數"
],
"data": [
[
"09:00:00",
"14,825.73",
"12,683.37",
"17,977.07",
"14,978.84",
"163.95",
"1,839.49",
"274.71",
"545.86",
"235.25",
"92.73",
"114.82",
"135.58",
"66.35",
"47.42",
"308.47",
"135.34",
"248.63",
"327.31",
"672.12",
"319.50",
"121.32",
"31.59",
"135.48",
"145.67",
"173.60",
"122.59",
"98.65",
"349.25",
"193.26",
"98.56",
"1,589.23",
"283.73",
"122.29",
"359.18"
],
......
],
"notes": [
"民國100年1月16日以前,資料為每一分鐘方式提供。",
"民國103年2月23日以前,資料為每15秒方式提供。",
"民國103年2月24日以後,資料為每10秒方式提供。",
"民國103年12月29日以後,資料為每5秒方式提供。"
]
}
打開終端機,我們先安裝 lodash
套件,方便我們計算出指數行情的開盤價、最高價、最低價與收盤價:
$ npm install --save lodash
$ npm install --save-dev @types/lodash
安裝完畢後,開啟 src/scraper/twse-scraper.service.ts
檔案,在 TwseScraperService
實作 fetchIndicesQuotes()
方法,取得集中市場指數行情:
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 { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class TwseScraperService {
constructor(private httpService: HttpService) {}
...
async fetchIndicesQuotes(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/MI_5MINS_INDEX?${query}`;
// 取得回應資料
const responseData = await firstValueFrom(this.httpService.get(url))
.then(response => (response.data.stat === 'OK') ? response.data : null);
// 若該日期非交易日或尚無成交資訊則回傳 null
if (!responseData) return null;
// 整理每 5 秒指數統計數據
const quotes = responseData.data.reduce((quotes, row) => {
const [
time, // 時間
IX0001, // 發行量加權股價指數
IX0007, // 未含金融保險股指數
IX0008, // 未含電子股指數
IX0009, // 未含金融電子股指數
IX0010, // 水泥類指數
IX0011, // 食品類指數
IX0012, // 塑膠類指數
IX0016, // 紡織纖維類指數
IX0017, // 電機機械類指數
IX0018, // 電器電纜類指數
IX0019, // 化學生技醫療類指數
IX0020, // 化學類指數
IX0021, // 生技醫療類指數
IX0022, // 玻璃陶瓷類指數
IX0023, // 造紙類指數
IX0024, // 鋼鐵類指數
IX0025, // 橡膠類指數
IX0026, // 汽車類指數
IX0027, // 電子類指數
IX0028, // 半導體類指數
IX0029, // 電腦及週邊設備類指數
IX0030, // 光電類指數
IX0031, // 通信網路類指數
IX0032, // 電子零組件類指數
IX0033, // 電子通路類指數
IX0034, // 資訊服務類指數
IX0035, // 其他電子類指數
IX0036, // 建材營造類指數
IX0037, // 航運類指數
IX0038, // 觀光類指數
IX0039, // 金融保險類指數
IX0040, // 貿易百貨類指數
IX0041, // 油電燃氣類指數
IX0042, // 其他類指數
] = row;
return [
...quotes,
{ date, time, symbol: 'IX0001', name: '發行量加權股價指數', price: numeral(IX0001).value()},
{ date, time, symbol: 'IX0007', name: '未含金融保險股指數', price: numeral(IX0007).value()},
{ date, time, symbol: 'IX0008', name: '未含電子股指數', price: numeral(IX0008).value()},
{ date, time, symbol: 'IX0009', name: '未含金融電子股指數', price: numeral(IX0009).value()},
{ date, time, symbol: 'IX0010', name: '水泥類指數', price: numeral(IX0010).value()},
{ date, time, symbol: 'IX0011', name: '食品類指數', price: numeral(IX0011).value()},
{ date, time, symbol: 'IX0012', name: '塑膠類指數', price: numeral(IX0012).value()},
{ date, time, symbol: 'IX0016', name: '紡織纖維類指數', price: numeral(IX0016).value()},
{ date, time, symbol: 'IX0017', name: '電機機械類指數', price: numeral(IX0017).value()},
{ date, time, symbol: 'IX0018', name: '電器電纜類指數', price: numeral(IX0018).value()},
{ date, time, symbol: 'IX0019', name: '化學生技醫療類指數', price: numeral(IX0019).value()},
{ date, time, symbol: 'IX0020', name: '化學類指數', price: numeral(IX0020).value()},
{ date, time, symbol: 'IX0021', name: '生技醫療類指數', price: numeral(IX0021).value()},
{ date, time, symbol: 'IX0022', name: '玻璃陶瓷類指數', price: numeral(IX0022).value()},
{ date, time, symbol: 'IX0023', name: '造紙類指數', price: numeral(IX0023).value()},
{ date, time, symbol: 'IX0024', name: '鋼鐵類指數', price: numeral(IX0024).value()},
{ date, time, symbol: 'IX0025', name: '橡膠類指數', price: numeral(IX0025).value()},
{ date, time, symbol: 'IX0026', name: '汽車類指數', price: numeral(IX0026).value()},
{ date, time, symbol: 'IX0027', name: '電子工業類指數', price: numeral(IX0027).value()},
{ date, time, symbol: 'IX0028', name: '半導體類指數', price: numeral(IX0028).value()},
{ date, time, symbol: 'IX0029', name: '電腦及週邊設備類指數', price: numeral(IX0029).value()},
{ date, time, symbol: 'IX0030', name: '光電類指數', price: numeral(IX0030).value()},
{ date, time, symbol: 'IX0031', name: '通信網路類指數', price: numeral(IX0031).value()},
{ date, time, symbol: 'IX0032', name: '電子零組件類指數', price: numeral(IX0032).value()},
{ date, time, symbol: 'IX0033', name: '電子通路類指數', price: numeral(IX0033).value()},
{ date, time, symbol: 'IX0034', name: '資訊服務類指數', price: numeral(IX0034).value()},
{ date, time, symbol: 'IX0035', name: '其他電子類指數', price: numeral(IX0035).value()},
{ date, time, symbol: 'IX0036', name: '建材營造類指數', price: numeral(IX0036).value()},
{ date, time, symbol: 'IX0037', name: '航運類指數', price: numeral(IX0037).value()},
{ date, time, symbol: 'IX0038', name: '觀光類指數', price: numeral(IX0038).value()},
{ date, time, symbol: 'IX0039', name: '金融保險類指數', price: numeral(IX0039).value()},
{ date, time, symbol: 'IX0040', name: '貿易百貨類指數', price: numeral(IX0040).value()},
{ date, time, symbol: 'IX0041', name: '油電燃氣類指數', price: numeral(IX0041).value()},
{ date, time, symbol: 'IX0042', name: '其他類指數', price: numeral(IX0042).value()},
];
}, []);
// 計算開高低收以及漲跌幅
const data = _(quotes)
.groupBy('symbol')
.map((data: any[]) => {
const [ prev, ...quotes ] = data;
const { date, symbol, name } = prev;
const openPrice = _.minBy(quotes, 'time').price;
const highPrice = _.maxBy(quotes, 'price').price;
const lowPrice = _.minBy(quotes, 'price').price;
const closePrice = _.maxBy(quotes, 'time').price;
const referencePrice = prev.price; // 取前次收盤價為參考價
const change = numeral(closePrice).subtract(referencePrice).value();
const changePercent = +numeral(change).divide(referencePrice).multiply(100).format('0.00');
return {
date, // 日期
symbol, // 指數代號
name, // 指數名稱
openPrice, // 開盤價
highPrice, // 最高價
lowPrice, // 最低價
closePrice, // 收盤價
change, // 漲跌
changePercent, // 漲跌幅
};
})
.value();
return data;
}
}
在 fetchIndicesQuotes()
方法中,需要指定 date
參數,表示要取得集中市場指數行情的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一個指數行情的物件,物件欄位包含如下:
date
:日期symbol
:指數代碼name
:指數名稱openPrice
:開盤價highPrice
:最高價lowPrice
:最低價closePrice
:收盤價change
:漲跌changePercent
:漲跌幅完成後,我們只要呼叫 TwseScraperService
的 fetchIndicesQuotes()
方法,就可以按日期取得集中市場指數行情。以日期 2022-07-01
為例:
[
{
date: '2022-07-01',
symbol: 'IX0001',
name: '發行量加權股價指數',
openPrice: 14812.13,
highPrice: 14812.13,
lowPrice: 14336.03,
closePrice: 14343.08,
change: -482.65,
changePercent: -3.26
},
{
date: '2022-07-01',
symbol: 'IX0007',
name: '未含金融保險股指數',
openPrice: 12671.45,
highPrice: 12671.45,
lowPrice: 12231.49,
closePrice: 12238.98,
change: -444.39,
changePercent: -3.5
},
{
date: '2022-07-01',
symbol: 'IX0008',
name: '未含電子股指數',
openPrice: 17982.49,
highPrice: 17989.35,
lowPrice: 17591.52,
closePrice: 17601.34,
change: -375.73,
changePercent: -2.09
},
......
]
在交易日盤中,櫃買市場每 5 秒會更新一次指數行情,並於盤後會公布數據。
在櫃買中心網站的 每5秒盤後統計 頁面,可以按日查詢加權指數及產業分類股價指數行情。
櫃買中心首頁 > 指數系列 > 櫃買指數暨產業分類指數 > 每5秒盤後統計
在「每5秒盤後統計」頁面選取「資料日期」並按下「查詢」後,就會列出該日每 5 秒指數盤後統計。
這裡需要注意,時間 09:00:00
列出的是前次交易日的指數收盤數字;時間 09:00:05
才是當日的指數開盤數字。
點擊「列印/匯出HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「111/07/01」,我們會得到以下 URL:
https://www.tpex.org.tw/web/stock/iNdex_info/minute_index/1MIN_print.php?l=zh-tw&d=111/07/01&s=0,asc,0
我們會發現這裡跟之前取得資料的方式有點不大一樣,因為我們無法直接指定輸出格式得到 JSON 格式資料。
回到「每5秒盤後統計」頁面。如果使用 Google Chrome 瀏覽器的開發者工具,在「網路」面板下會顯示所有網路請求詳細訊息的紀錄:
從以上紀錄我們可以得知,櫃買中心「每5秒盤後統計」頁面是請求了以下位址得到資料:
https://www.tpex.org.tw/web/stock/iNdex_info/minute_index/1MIN_result.php?l=zh-tw&d=111/07/01
以上 URL 可設定的參數如下:
l
:輸出資料的語系。zh-tw
為正體中文;en-us
為英文。d
:資料日期。接受 民國年/月/日
的日期格式。需要注意,若 l
參數指定為 en-us
,則 d
參數需改成 西元年/月/日
的日期格式。我們將 URL 查詢參數改為 l=zh-tw&d=111/07/01
,櫃買中心就會以 JSON 格式資料回應 2022 年 7 月 1 日的櫃買市場指數行情:
{
"reportDate": "111/07/01",
"iTotalRecords": 3242,
"csvTitle": "\u6bcf 5 \u79d2\u76e4\u5f8c\u7d71\u8a08",
"aaData": [
[
"09:00:00",
"103.17",
"145.76",
"147.31",
"161.3",
"167.77",
"66.85",
"111.65",
"129.67",
"184.64",
"90.43",
"74.09",
"52.49",
"74.69",
"85.49",
"84.15",
"107.19",
"130.47",
"105.08",
"275.99",
"181.09",
"0",
"0",
"0",
"92,418",
"128,235",
"901,248",
"186,339"
],
......
]
}
開啟 src/scraper/tpex-scraper.service.ts
檔案,在 TpexScraperService
實作 fetchIndicesQuotes()
方法,取得櫃買市場指數行情:
import * as _ from 'lodash';
import * as numeral from 'numeral';
import { DateTime } from 'luxon';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class TpexScraperService {
constructor(private httpService: HttpService) {}
...
async fetchIndicesQuotes(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/iNdex_info/minute_index/1MIN_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;
// 整理每 5 秒指數統計數據
const quotes = responseData.aaData.reduce((quotes, row) => {
const [
time, // 時間
IX0044, // 櫃檯紡纖類指數
IX0045, // 櫃檯機械類指數
IX0046, // 櫃檯鋼鐵類指數
IX0048, // 櫃檯營建類指數
IX0049, // 櫃檯航運類指數
IX0050, // 櫃檯觀光類指數
IX0100, // 櫃檯其他類指數
IX0051, // 櫃檯化工類指數
IX0052, // 櫃檯生技醫療類指數
IX0053, // 櫃檯半導體類指數
IX0054, // 櫃檯電腦及週邊類指數
IX0055, // 櫃檯光電業類指數
IX0056, // 櫃檯通信網路類指數
IX0057, // 櫃檯電子零組件類指數
IX0058, // 櫃檯電子通路類指數
IX0059, // 櫃檯資訊服務類指數
IX0099, // 櫃檯其他電子類指數
IX0075, // 櫃檯文化創意業類指數
IX0047, // 櫃檯電子類指數
IX0043, // 櫃檯指數
tradeValue, // 成交金額(萬元)
tradeVolume, // 成交張數
transaction, // 成交筆數
bidOrders, // 委買筆數
askOrders, // 委賣筆數
bidVolume, // 委買張數
askVolume, // 委賣張數
] = row;
return [
...quotes,
{ date, time, symbol: 'IX0044', name: '櫃檯紡纖類指數', price: numeral(IX0044).value() },
{ date, time, symbol: 'IX0045', name: '櫃檯機械類指數', price: numeral(IX0045).value() },
{ date, time, symbol: 'IX0046', name: '櫃檯鋼鐵類指數', price: numeral(IX0046).value() },
{ date, time, symbol: 'IX0048', name: '櫃檯營建類指數', price: numeral(IX0048).value() },
{ date, time, symbol: 'IX0049', name: '櫃檯航運類指數', price: numeral(IX0049).value() },
{ date, time, symbol: 'IX0050', name: '櫃檯觀光類指數', price: numeral(IX0050).value() },
{ date, time, symbol: 'IX0100', name: '櫃檯其他類指數', price: numeral(IX0100).value() },
{ date, time, symbol: 'IX0051', name: '櫃檯化工類指數', price: numeral(IX0051).value() },
{ date, time, symbol: 'IX0052', name: '櫃檯生技醫療類指數', price: numeral(IX0052).value() },
{ date, time, symbol: 'IX0053', name: '櫃檯半導體類指數', price: numeral(IX0053).value() },
{ date, time, symbol: 'IX0054', name: '櫃檯電腦及週邊類指數', price: numeral(IX0054).value() },
{ date, time, symbol: 'IX0055', name: '櫃檯光電業類指數', price: numeral(IX0055).value() },
{ date, time, symbol: 'IX0056', name: '櫃檯通信網路類指數', price: numeral(IX0056).value() },
{ date, time, symbol: 'IX0057', name: '櫃檯電子零組件類指數', price: numeral(IX0057).value() },
{ date, time, symbol: 'IX0058', name: '櫃檯電子通路類指數', price: numeral(IX0058).value() },
{ date, time, symbol: 'IX0059', name: '櫃檯資訊服務類指數', price: numeral(IX0059).value() },
{ date, time, symbol: 'IX0099', name: '櫃檯其他電子類指數', price: numeral(IX0099).value() },
{ date, time, symbol: 'IX0075', name: '櫃檯文化創意業類指數', price: numeral(IX0075).value() },
{ date, time, symbol: 'IX0047', name: '櫃檯電子類指數', price: numeral(IX0047).value() },
{ date, time, symbol: 'IX0043', name: '櫃檯指數', price: numeral(IX0043).value() },
];
}, []);
// 計算開高低收以及漲跌幅
const data = _(quotes)
.groupBy('symbol')
.map((data: any[]) => {
const [ prev, ...quotes ] = data;
const { date, symbol, name } = prev;
const openPrice = _.minBy(quotes, 'time').price;
const highPrice = _.maxBy(quotes, 'price').price;
const lowPrice = _.minBy(quotes, 'price').price;
const closePrice = _.maxBy(quotes, 'time').price;
const referencePrice = prev.price;
const change = numeral(closePrice).subtract(referencePrice).value();
const changePercent = +numeral(change).divide(referencePrice).multiply(100).format('0.00');
return {
date, // 日期
symbol, // 指數代號
name, // 指數名稱
openPrice, // 開盤價
highPrice, // 最高價
lowPrice, // 最低價
closePrice, // 收盤價
change, // 漲跌
changePercent, // 漲跌幅
};
})
.value();
return data;
}
}
在 fetchIndicesQuotes()
方法中,需要指定 date
參數,表示要取得櫃買市場指數行情的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一個指數行情的物件,物件欄位包含如下:
date
:日期symbol
:指數代碼name
:指數名稱openPrice
:開盤價highPrice
:最高價lowPrice
:最低價closePrice
:收盤價change
:漲跌changePercent
:漲跌幅完成後,我們只要呼叫 TwseScraperService
的 fetchIndicesQuotes()
方法,就可以按日期取得櫃買市場指數行情。以日期 2022-07-01
為例:
[
{
date: '2022-07-01',
symbol: 'IX0044',
name: '櫃檯紡纖類指數',
openPrice: 103.17,
highPrice: 103.17,
lowPrice: 100.41,
closePrice: 100.5,
change: -2.67,
changePercent: -2.59
},
{
date: '2022-07-01',
symbol: 'IX0045',
name: '櫃檯機械類指數',
openPrice: 145.82,
highPrice: 146.15,
lowPrice: 141.6,
closePrice: 141.65,
change: -4.11,
changePercent: -2.82
},
......
{
date: '2022-07-01',
symbol: 'IX0043',
name: '櫃檯指數',
openPrice: 180.71,
highPrice: 180.91,
lowPrice: 173.03,
closePrice: 173.03,
change: -8.06,
changePercent: -4.45
}
}
本系列文已正式出版為《Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰》。本書新增了全新內容和實用範例,為你提供更深入的學習體驗!歡迎參考選購,開始你的量化投資之旅!
天瓏網路書店連結:https://www.tenlong.com.tw/products/9786263336070