前面我們已經介紹評估大盤狀況的方法,並透過相對強度分析和市場資金流向來找出強勢和弱勢的產業類股,接下來我們要進入由上而下投資法的 第三階段,也就是 個股 的主題。每天股票市場開盤至收盤,都會產生四個價位,即 開盤價、最高價、最低價 以及 收盤價,而 K線 正是由這四個價格所組成。今天我們會介紹投資人應該具備的K線基礎,然後示範如何從證交所及櫃買中心網站取得上市櫃股票行情資訊。
K線 也有人稱為 K棒,因為形狀像蠟燭,也有人稱為 蠟燭線、陰陽線、紅黑線 等。一根單一K線,是由一天或某一週期的股價走勢所形成的四個價位繪製而成,分別是「開盤價」、「最高價」、「最低價」、「收盤價」。
根據開盤價與收盤價的高低,K線又可以分為「紅K線」與「黑K線」。當收盤價高於開盤價,代表當天走勢開低走高,會以 紅K線 表示,也稱為 陽線;而收盤價低於開盤價,代表當天走勢開高走低,則以 黑K線 表示,也稱作 陰線。
如果當日最高價高於紅K線的收盤價,或是高於黑K線的開盤價,就會出現「上影線」;如果當日最低價低於紅K線的開盤價,或是低於黑K線的收盤價,就會出現「下影線」。
只有少部分的亞洲股市採用紅漲綠跌;歐美股市都是採用綠漲紅跌。因為漲跌顏色不同,K棒的顏色意義也是反過來的,所以在觀察外國股市走勢的時候,需要注意提供資訊的網站或看盤軟體的設定。
K線依據時間架構可以分為「日K線」、「週K線」、「月K線」:
不同週期的投資者會參考不同時間架構的K線,通常波段投資人較長參考的是 日K線 及 週K線,如果是當沖或極短線交易者,可能還會參考 1 分K線、5 分K線、15 分K線、30 分K線 等。
K線圖是由多個K線所組成,將某段期間內的相同時間單位的K線組合起來,就可以畫出K線圖,橫軸為時間,縱軸為價格。除了價格外,成交量也透露出重要訊息,反映出該商品交易的熱度以及群眾的參與程度。成交量通常以柱狀圖表示並畫在K線圖下方,柱狀圖的高度就代表成交量的大小。
Source:TradingView
由於K線是技術分析的基礎,價格也是所有買賣交易人最在意的事。在進行選股之前,我們務必瞭解如何取得上市櫃股票行情,包含開盤價、最高價、最低價、收盤價以及成交量等資訊。
在證交所 每日收盤行情 頁面,可以按日查詢集中市場個股收盤行情。
證交所首頁 > 交易資訊 > 盤後資訊 > 每日收盤行情
在「每日收盤行情」頁面選取要查詢的「日期」,因為我們是要查詢個股不需包含權證,在「分類項目」選擇「全部(不含權證、牛熊證、可展延牛熊證)」,然後按下「查詢」。
拉到最下面可以看到每檔股票的收盤行情:
回到頁面上方,點擊「列印 / HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「民國 111 年 07 月 01 日」,我們會得到以下 URL:
https://www.twse.com.tw/exchangeReport/MI_INDEX?response=html&date=20220701&type=ALLBUT0999
以上 URL 可設定的參數如下:
response
:回應資料的格式。指定 html
輸出 HTML 文件;改為 csv
可以另存 CSV 檔案;設定成 json
或不指定則回應 JSON 格式資料。date
:資料日期。接受的日期格式為 yyyyMMdd
,如 20220701
。type
:分類項目。 ALLBUT0999
表示分類項目是「全部(不含權證、牛熊證、可展延牛熊證)」。我們將 URL 查詢參數改為 response=json&date=20220701&type=ALLBUT0999
,證交所就會以 JSON 格式資料回應 2022 年 7 月 1 日的個股收盤行情:
{
......
"fields9": [
"證券代號",
"證券名稱",
"成交股數",
"成交筆數",
"成交金額",
"開盤價",
"最高價",
"最低價",
"收盤價",
"漲跌(+/-)",
"漲跌價差",
"最後揭示買價",
"最後揭示買量",
"最後揭示賣價",
"最後揭示賣量",
"本益比"
],
......
"data9": [
[
"0050",
"元大台灣50",
"36,878,756",
"51,585",
"4,168,809,309",
"115.65",
"115.65",
"111.20",
"111.55",
"<p style= color:green>-</p>",
"4.25",
"111.55",
"29",
"111.60",
"4",
"0.00"
],
......
]
}
因為回應資料較長,以上我們簡化了資料只顯示重要的部分。我們要取得的個股行情是放在 JSON 欄位 data9
的內容,其陣列的每個元素即代表一檔個股行情資訊。
在專案目錄下開啟 src/scraper/twse-scraper.service.ts
檔案,在 TwseScraperService
實作 fetchEquitiesQuotes()
方法,取得集中市場個股行情:
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 fetchEquitiesQuotes(date: string) {
// 將 `date` 轉換成 `yyyyMMdd` 格式
const formattedDate = DateTime.fromISO(date).toFormat('yyyyMMdd');
// 建立 URL 查詢參數
const query = new URLSearchParams({
response: 'json', // 指定回應格式為 JSON
date: formattedDate, // 指定資料日期
type: 'ALLBUT0999', // 指定分類項目為全部(不含權證、牛熊證、可展延牛熊證)
});
const url = `https://www.twse.com.tw/exchangeReport/MI_INDEX?${query}`;
// 取得回應資料
const responseData = await firstValueFrom(this.httpService.get(url))
.then(response => (response.data.stat === 'OK') ? response.data : null);
// 若該日期非交易日或尚無成交資訊則回傳 null
if (!responseData) return null;
// 整理回應資料
const data = responseData.data9.map(row => {
const [ symbol, name, ...values ] = row;
const [
tradeVolume, // 成交股數
transaction, // 成交筆數
tradeValue, // 成交金額
openPrice, // 開盤價
highPrice, // 最高價
lowPrice, // 最低價
closePrice, // 收盤價
] = values.slice(0, 7).map(value => numeral(value).value());
// 計算漲跌
const change = values[7].includes('green')
? numeral(values[8]).multiply(-1).value()
: numeral(values[8]).value();
// 回推參考價
const referencePrice = closePrice && numeral(closePrice).subtract(change).value();
// 計算漲跌幅
const changePercent = closePrice && +numeral(change).divide(referencePrice).multiply(100).format('0.00');
return {
date,
symbol,
name,
openPrice,
highPrice,
lowPrice,
closePrice,
change,
changePercent,
tradeVolume,
tradeValue,
transaction,
};
});
return data;
}
}
在 fetchEquitiesQuotes()
方法中,需要指定 date
參數,表示要取得集中市場個股行情的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一檔股票的物件,物件欄位包含如下:
date
:日期symbol
:股票代號name
:股票名稱openPrice
:開盤價highPrice
:最高價lowPrice
:最低價closePrice
:收盤價change
:漲跌changePercent
:漲跌幅tradeVolume
:成交股數tradeValue
:成交金額transaction
:成交筆數完成後,我們只要呼叫 TwseScraperService
的 fetchEquitiesQuotes()
方法,就可以按日期取得集中市場個股行情。以日期 2022-07-01
為例:
[
{
date: '2022-07-01',
symbol: '0050',
name: '元大台灣50',
openPrice: 115.65,
highPrice: 115.65,
lowPrice: 111.2,
closePrice: 111.55,
change: -4.25,
changePercent: -3.67,
tradeVolume: 36878756,
tradeValue: 4168809309,
transaction: 51585
},
... 1167 more items
]
在櫃買中心網站的 上櫃股票行情 頁面,可以按日查詢上櫃股票收盤行情。
櫃買中心首頁 > 上櫃 > 盤後資訊 > 上櫃股票行情
在「上櫃股票行情」頁面選取「資料日期」後,就會列出該日上櫃股票收盤行情。
點擊「列印/匯出HTML」連結,瀏覽器會開新分頁將資訊輸出成可列印的 HTML 頁面。假設資料日期為「111/07/01」,我們會得到以下 URL:
https://www.tpex.org.tw/web/stock/aftertrading/daily_close_quotes/stk_quote_result.php?l=zh-tw&o=htm&d=111/07/01&s=0,asc,0
以上 URL 可設定的參數如下:
l
:輸出資料的語系。zh-tw
為正體中文;en-us
為英文。d
:資料日期。接受 民國年/月/日
的日期格式。需要注意,若 l
參數指定為 en-us
,則 d
參數需改成 西元年/月/日
的日期格式。s
:指定欄位要依照升冪或降冪排序。例如 0,asc,0
是按代號升冪排序;1,desc,0
則按名稱降冪排序,依此類推。o
:資料輸出的格式。指定 htm
表示輸出 HTML 文件;改為 csv
可以另存 CSV 檔案;設定成 json
或不指定則回應 JSON 格式資料。我們將 URL 查詢參數改為 l=zh-tw&o=json&d=111/07/01
,櫃買中心就會以 JSON 格式資料回應 2022 年 7 月 1 日的上櫃股票行情:
{
"reportDate": "111/07/01",
"reportTitle": "上櫃股票行情(含等價、零股、盤後、鉅額交易)",
"iTotalRecords": 7747,
"iTotalDisplayRecords": 7747,
"colNum": 19,
"listNum": "799",
"totalAmount": "67,619,220,358",
"totalVolumn": "845,320,570",
"totalCount": "577,205",
"mmData": [],
"aaData": [
[
"006201",
"元大富櫃50",
"15.26",
"-0.84 ",
"16.10",
"16.10",
"15.26",
"15.58",
"210,613",
"3,281,721",
"167",
"15.25",
"7",
"15.26",
"6",
"17,446,000",
"15.26",
"16.78",
"13.74"
],
......
]
}
以上回應資料 aaData
欄位的陣列中每個索引值的元素,依序表示為股票代號、股票名稱、收盤價、漲跌、開盤價、最高價、最低價、成交均價、成交股數、成交金額(元)、成交筆數、最後買價、最後買量(千股)、最後賣價、最後賣量(千股)、發行股數、次日參考價、次日漲停價、次日跌停價。
因為櫃買中心回傳的上櫃股票收盤行情包含權證,所以我們先實作一個工具函式用來判斷證券代號為權證與否。
我們在 libs/common/src/utils
目錄下新增 is-warrant.util.ts
檔案,實作 isWarrant()
工具函示:
export function isWarrant(symbol: string): boolean {
const rules = [
/^0[3-8][0-9][0-9][0-9][0-9]$/, // 上市國內標的認購權證
/^0[3-8][0-9][0-9][0-9]P$/, // 上市國內標的認售權證
/^0[3-8][0-9][0-9][0-9]F$/, // 上市外國標的認購權證
/^0[3-8][0-9][0-9][0-9]Q$/, // 上市外國標的認售權證
/^0[3-8][0-9][0-9][0-9]C$/, // 上市國內標的下限型認購權證
/^0[3-8][0-9][0-9][0-9]B$/, // 上市國內標的上限型認售權證
/^0[3-8][0-9][0-9][0-9]X$/, // 上市國內標的可展延下限型認購權證
/^0[3-8][0-9][0-9][0-9]Y$/, // 上市國內標的可展延上限型認售權證
/^7[0-3][0-9][0-9][0-9][0-9]$/, // 上櫃國內標的認購權證
/^7[0-3][0-9][0-9][0-9]P$/, // 上櫃國內標的認售權證
/^7[0-3][0-9][0-9][0-9]F$/, // 上櫃外國標的認購權證
/^7[0-3][0-9][0-9][0-9]Q$/, // 上櫃外國標的認售權證
/^7[0-3][0-9][0-9][0-9]C$/, // 上櫃國內標的下限型認購權證
/^7[0-3][0-9][0-9][0-9]B$/, // 上櫃國內標的上限型認售權證
/^7[0-3][0-9][0-9][0-9]X$/, // 上櫃國內標的可展延下限型認購權證
/^7[0-3][0-9][0-9][0-9]Y$/, // 上櫃國內標的可展延上限型認售權證
]
return rules.some(regex => regex.test(symbol));
}
因為權證代號有一定的編碼規則,我們實作 isWarrant()
函式接受有價證券代號,依照有價證券代號的規則判斷是否為權證。
完成後,開啟 libs/common/src/utils/index.ts
檔案,將 is-warrant.util.ts
檔案匯出:
export * from './is-warrant.util';
之後就可以在 Nest 應用程式透過以下方式引用 isWarrant()
函式:
import { isWarrant } from '@speculator/common'
完成 isWarrant()
工具函式後,我們要實作取得上櫃個股行情。開啟 src/scraper/tpex-scraper.service.ts
檔案,在 TpexScraperService
實作 fetchEquitiesQuotes()
方法,取得櫃買市場個股行情:
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, isWarrant } from '@speculator/common';
@Injectable()
export class TpexScraperService {
constructor(private httpService: HttpService) {}
async fetchEquitiesQuotes(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/aftertrading/daily_close_quotes/stk_quote_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 data = responseData.aaData
.filter(row => !isWarrant(row[0])) // 過濾權證
.map(row => {
const [ symbol, name, ...values ] = row;
const [
closePrice, // 收盤價
change, // 漲跌
openPrice, // 開盤價
highPrice, // 最高價
lowPrice, // 最低價
avgPrice, // 均價
tradeVolume, // 成交股數
tradeValue, // 成交金額
transaction, // 成交筆數
] = values.slice(0, 9).map(value => numeral(value).value());
// 回推參考價
const referencePrice = (closePrice && change !== null) && numeral(closePrice).subtract(change).value() || null;
// 計算漲跌幅
const changePercent = (closePrice && change !== null) && +numeral(change).divide(referencePrice).multiply(100).format('0.00') || null;
return {
date,
symbol,
name,
openPrice,
highPrice,
lowPrice,
closePrice,
change,
changePercent,
tradeVolume,
tradeValue,
transaction,
};
});
return data;
}
}
在 fetchEquitiesQuotes()
方法中,需要指定 date
參數,表示要取得櫃買市場個股行情的日期。我們定義回傳的型別是一個陣列,每個陣列元素代表一檔股票的物件,物件欄位包含如下:
date
:日期symbol
:股票代號name
:股票名稱openPrice
:開盤價highPrice
:最高價lowPrice
:最低價closePrice
:收盤價change
:漲跌changePercent
:漲跌幅tradeVolume
:成交股數tradeValue
:成交金額transaction
:成交筆數完成後,我們只要呼叫 TpexScraperService
的 fetchEquitiesQuotes()
方法,就可以按日期取得櫃買市場個股行情。以日期 2022-07-01
為例:
[
{
date: '2022-07-01',
symbol: '006201',
name: '元大富櫃50',
openPrice: 16.1,
highPrice: 16.1,
lowPrice: 15.26,
closePrice: 15.26,
change: -0.84,
changePercent: -5.22,
tradeVolume: 210613,
tradeValue: 3281721,
transaction: 167
},
... 905 more items
]
本系列文已正式出版為《Node.js 量化投資全攻略:從資料收集到自動化交易系統建構實戰》。本書新增了全新內容和實用範例,為你提供更深入的學習體驗!歡迎參考選購,開始你的量化投資之旅!
天瓏網路書店連結:https://www.tenlong.com.tw/products/9786263336070