UNPKG

node-twstock

Version:

A client library for scraping Taiwan stock market data

661 lines (660 loc) 31.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TwseScraper = void 0; const _ = require("lodash"); const numeral = require("numeral"); const luxon_1 = require("luxon"); const scraper_1 = require("./scraper"); const enums_1 = require("../enums"); const utils_1 = require("../utils"); class TwseScraper extends scraper_1.Scraper { async fetchStocksHistorical(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), type: 'ALLBUT0999', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?${query}`; const response = await this.httpService.get(url, { headers: { 'Connection': 'keep-alive' } }); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = json.tables[8].data.map((row) => { const [symbol, name, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.open = numeral(values[3]).value(); data.high = numeral(values[4]).value(); data.low = numeral(values[5]).value(); data.close = numeral(values[6]).value(); data.volume = numeral(values[0]).value(); data.turnover = numeral(values[2]).value(); data.transaction = numeral(values[1]).value(); data.change = values[7].includes('green') ? numeral(values[8]).multiply(-1).value() : numeral(values[8]).value(); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksInstitutional(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), selectType: 'ALLBUT0999', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/fund/T86?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = json.data.map((row) => { const [symbol, name, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.institutional = ((values) => { switch (values.length) { case 17: return [ { investor: '外資及陸資(不含外資自營商)', totalBuy: numeral(values[0]).value(), totalSell: numeral(values[1]).value(), difference: numeral(values[2]).value(), }, { investor: '外資自營商', totalBuy: numeral(values[3]).value(), totalSell: numeral(values[4]).value(), difference: numeral(values[5]).value(), }, { investor: '投信', totalBuy: numeral(values[6]).value(), totalSell: numeral(values[7]).value(), difference: numeral(values[8]).value(), }, { investor: '自營商', difference: numeral(values[9]).value(), }, { investor: '自營商(自行買賣)', totalBuy: numeral(values[10]).value(), totalSell: numeral(values[11]).value(), difference: numeral(values[12]).value(), }, { investor: '自營商(避險)', totalBuy: numeral(values[13]).value(), totalSell: numeral(values[14]).value(), difference: numeral(values[15]).value(), }, { investor: '三大法人', difference: numeral(values[16]).value(), }, ]; case 14: return [ { investor: '外資及陸資', totalBuy: numeral(values[0]).value(), totalSell: numeral(values[1]).value(), difference: numeral(values[2]).value(), }, { investor: '投信', totalBuy: numeral(values[3]).value(), totalSell: numeral(values[4]).value(), difference: numeral(values[5]).value(), }, { investor: '自營商', difference: numeral(values[6]).value(), }, { investor: '自營商(自行買賣)', totalBuy: numeral(values[7]).value(), totalSell: numeral(values[8]).value(), difference: numeral(values[9]).value(), }, { investor: '自營商(避險)', totalBuy: numeral(values[10]).value(), totalSell: numeral(values[11]).value(), difference: numeral(values[12]).value(), }, { investor: '三大法人', difference: numeral(values[13]).value(), }, ]; case 10: return [ { investor: '外資及陸資', totalBuy: numeral(values[0]).value(), totalSell: numeral(values[1]).value(), difference: numeral(values[2]).value(), }, { investor: '投信', totalBuy: numeral(values[3]).value(), totalSell: numeral(values[4]).value(), difference: numeral(values[5]).value(), }, { investor: '自營商', totalBuy: numeral(values[6]).value(), totalSell: numeral(values[7]).value(), difference: numeral(values[8]).value(), }, { investor: '三大法人', difference: numeral(values[9]).value(), }, ]; } })(values); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksFiniHoldings(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), selectType: 'ALLBUT0999', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/fund/MI_QFIIS?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = json.data.map((row) => { const [symbol, name, isin, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.issuedShares = numeral(values[0]).value(); data.availableShares = numeral(values[1]).value(); data.sharesHeld = numeral(values[2]).value(); data.availablePercent = numeral(values[3]).value(); data.heldPercent = numeral(values[4]).value(); data.upperLimitPercent = numeral(values[5]).value(); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksMarginTrades(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), selectType: 'ALL', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/marginTrading/MI_MARGN?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = json.tables[1].data.map((row) => { const [symbol, name, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.marginBuy = numeral(values[0]).value(); data.marginSell = numeral(values[1]).value(); data.marginRedeem = numeral(values[2]).value(); data.marginBalancePrev = numeral(values[3]).value(); data.marginBalance = numeral(values[4]).value(); data.marginQuota = numeral(values[5]).value(); data.shortBuy = numeral(values[6]).value(); data.shortSell = numeral(values[7]).value(); data.shortRedeem = numeral(values[8]).value(); data.shortBalancePrev = numeral(values[9]).value(); data.shortBalance = numeral(values[10]).value(); data.shortQuota = numeral(values[11]).value(); data.offset = numeral(values[12]).value(); data.note = values[13].trim(); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksShortSales(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/marginTrading/TWT93U?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json.data.length) return null; const data = json.data.map((row) => { const [symbol, name, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.marginShortBalancePrev = numeral(values[0]).value(); data.marginShortSell = numeral(values[1]).value(); data.marginShortBuy = numeral(values[2]).value(); data.marginShortRedeem = numeral(values[3]).value(); data.marginShortBalance = numeral(values[4]).value(); data.marginShortQuota = numeral(values[5]).value(); data.sblShortBalancePrev = numeral(values[6]).value(); data.sblShortSale = numeral(values[7]).value(); data.sblShortReturn = numeral(values[8]).value(); data.sblShortAdjustment = numeral(values[9]).value(); data.sblShortBalance = numeral(values[10]).value(); data.sblShortQuota = numeral(values[11]).value(); data.note = values[12].trim(); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksValues(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), selectType: 'ALL', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/afterTrading/BWIBBU_d?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = json.data.map((row) => { const [symbol, name, ...values] = row; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.peRatio = numeral(values[2]).value(); data.pbRatio = numeral(values[3]).value(); data.dividendYield = numeral(values[0]).value(); data.dividendYear = numeral(values[1]).add(1911).value(); return data; }); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchStocksDividends(options) { const { startDate, endDate, symbol } = options; const query = new URLSearchParams({ startDate: luxon_1.DateTime.fromISO(startDate).toFormat('yyyyMMdd'), endDate: luxon_1.DateTime.fromISO(endDate).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/exRight/TWT49U?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'OK' && response.data; if (!json) return []; const data = await Promise.all(json.data.map(async (row) => { const [date, symbol, name, ...values] = row; const formattedDate = date.replace(/(\d+)年(\d+)月(\d+)日/, (_, year, month, day) => { const westernYear = parseInt(year) + 1911; return `${westernYear}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; }); const data = {}; data.date = formattedDate; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.previousClose = numeral(values[0]).value(); data.referencePrice = numeral(values[1]).value(); data.dividend = numeral(values[2]).value(); data.dividendType = values[3].trim(); data.limitUpPrice = numeral(values[4]).value(); data.limitDownPrice = numeral(values[5]).value(); data.openingReferencePrice = numeral(values[6]).value(); data.exdividendReferencePrice = numeral(values[7]).value(); const [_, detailDate] = values[8].split(','); const detail = await this.fetchStocksDividendsDetail({ symbol, date: detailDate }); return Object.assign(Object.assign({}, data), detail); })); return symbol ? data.filter((data) => data.symbol === symbol) : data; } async fetchStocksDividendsDetail(options) { const { date, symbol } = options; const query = new URLSearchParams({ STK_NO: symbol, T1: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/exRight/TWT49UDetail?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'ok' && response.data; if (!json) return null; const [_, name, ...values] = json.data[0]; const data = {}; data.symbol = symbol; data.name = name.trim(); data.cashDividend = values[0] && parseFloat(values[0]); data.stockDividendShares = values[2] && parseFloat(values[2]); return data; } async fetchStocksCapitalReductions(options) { const { startDate, endDate, symbol } = options; const query = new URLSearchParams({ startDate: luxon_1.DateTime.fromISO(startDate).toFormat('yyyyMMdd'), endDate: luxon_1.DateTime.fromISO(endDate).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/reducation/TWTAUU?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'OK' && response.data; if (!json) return []; const data = await Promise.all(json.data.map(async (row) => { const [date, symbol, name, ...values] = row; const [year, month, day] = date.split('/'); const data = {}; data.resumeDate = `${+year + 1911}-${month}-${day}`; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.previousClose = numeral(values[0]).value(); data.referencePrice = numeral(values[1]).value(); data.limitUpPrice = numeral(values[2]).value(); data.limitDownPrice = numeral(values[3]).value(); data.openingReferencePrice = numeral(values[4]).value(); data.exrightReferencePrice = numeral(values[5]).value(); data.reason = values[6].trim(); const [_, detailDate] = values[7].split(','); const detail = await this.fetchStockCapitalReductionDetail({ symbol, date: detailDate }); return Object.assign(Object.assign({}, data), detail); })); return symbol ? data.filter((data) => data.symbol === symbol) : data; } async fetchStockCapitalReductionDetail(options) { const { date, symbol } = options; const query = new URLSearchParams({ STK_NO: symbol, FILE_DATE: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/reducation/TWTAVUDetail?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'OK' && response.data; if (!json) return null; const [_, name, ...values] = json.data[0]; const [year, month, day] = values[0].split('/'); const data = {}; data.symbol = symbol; data.name = name.trim(); data.haltDate = `${+year + 1911}-${month}-${day}`; data.sharesPerThousand = parseFloat(values[1]); data.refundPerShare = parseFloat(values[2]); return data; } async fetchStocksSplits(options) { const { startDate, endDate, symbol } = options; const query = new URLSearchParams({ startDate: luxon_1.DateTime.fromISO(startDate).toFormat('yyyyMMdd'), endDate: luxon_1.DateTime.fromISO(endDate).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/change/TWTB8U?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'OK' && response.data; const data = json.data.map((row) => { const [date, symbol, name, ...values] = row; const [year, month, day] = date.split('/'); const data = {}; data.resumeDate = `${+year + 1911}-${month}-${day}`; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.previousClose = numeral(values[0]).value(); data.referencePrice = numeral(values[1]).value(); data.limitUpPrice = numeral(values[2]).value(); data.limitDownPrice = numeral(values[3]).value(); data.openingReferencePrice = numeral(values[4]).value(); return data; }); return symbol ? data.filter((data) => data.symbol === symbol) : data; } async fetchStocksEtfSplits(options) { const { startDate, endDate, symbol } = options; const query = new URLSearchParams({ startDate: luxon_1.DateTime.fromISO(startDate).toFormat('yyyyMMdd'), endDate: luxon_1.DateTime.fromISO(endDate).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/split/TWTCAU?${query}`; const response = await this.httpService.get(url); const json = response.data.stat === 'OK' && response.data; const data = json.data.map((row) => { const [date, symbol, name, type, ...values] = row; const [year, month, day] = date.split('/'); const data = {}; data.resumeDate = `${+year + 1911}-${month}-${day}`; data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.type = type; data.previousClose = numeral(values[0]).value(); data.referencePrice = numeral(values[1]).value(); data.limitUpPrice = numeral(values[2]).value(); data.limitDownPrice = numeral(values[3]).value(); data.openingReferencePrice = numeral(values[4]).value(); return data; }) .filter((row) => options.reverseSplit ? row.type === '反分割' : row.type === '分割') .map((row) => _.omit(row, ['type'])); return symbol ? data.filter((data) => data.symbol === symbol) : data; } async fetchIndicesHistorical(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/TAIEX/MI_5MINS_INDEX?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const indices = json.fields.slice(1).map((index) => ({ symbol: (0, utils_1.asIndex)(index), name: index, })); const quotes = json.data.flatMap((row) => { const [time, ...values] = row; return values.map((value, i) => ({ date, time, symbol: indices[i].symbol, name: indices[i].name, price: numeral(value).value(), })); }); const data = _(quotes).groupBy('symbol') .map(quotes => { const [prev, ...rows] = quotes; const { date, symbol, name } = prev; const data = {}; data.date = date, data.exchange = enums_1.Exchange.TWSE; data.symbol = symbol; data.name = name.trim(); data.open = _.minBy(rows, 'time').price; data.high = _.maxBy(rows, 'price').price; data.low = _.minBy(rows, 'price').price; data.close = _.maxBy(rows, 'time').price; data.change = numeral(data.close).subtract(prev.price).value(); return data; }).value(); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchIndicesTrades(options) { const { date, symbol } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/afterTrading/BFIAMU?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const market = await this.fetchMarketTrades({ date }); if (!market) return null; const data = json.data.map((row) => { const data = {}; data.date = date, data.exchange = enums_1.Exchange.TWSE; data.symbol = (0, utils_1.asIndex)(row[0].trim()); data.name = row[0].trim(); data.tradeVolume = numeral(row[1]).value(); data.tradeValue = numeral(row[2]).value(); data.tradeWeight = +numeral(data.tradeValue).divide(market.tradeValue).multiply(100).format('0.00'); return data; }); const excludedSymbols = [enums_1.Index.ChemicalBiotechnologyAndMedicalCare, enums_1.Index.Electronics]; const total = data .filter(row => !excludedSymbols.includes(row.symbol)) .reduce((total, row) => ({ tradeVolume: total.tradeVolume + row.tradeVolume, tradeValue: total.tradeValue + row.tradeValue, }), { tradeVolume: 0, tradeValue: 0 }); const electronics = _.find(data, { symbol: enums_1.Index.Electronics }); const finance = _.find(data, { symbol: enums_1.Index.FinancialAndInsurance }); const createIndexEntry = (symbol, name, tradeVolume, tradeValue) => ({ date, exchange: enums_1.Exchange.TWSE, symbol, name, tradeVolume, tradeValue, tradeWeight: +numeral(tradeValue).divide(market.tradeValue).multiply(100).format('0.00') }); const nonFinance = createIndexEntry(enums_1.Index.NonFinance, '未含金融保險股指數', total.tradeVolume - finance.tradeVolume, total.tradeValue - finance.tradeValue); const nonElectronics = createIndexEntry(enums_1.Index.NonElectronics, '未含電子股指數', total.tradeVolume - electronics.tradeVolume, total.tradeValue - electronics.tradeValue); const nonFinanceNonElectronics = createIndexEntry(enums_1.Index.NonFinanceNonElectronics, '未含金融電子股指數', total.tradeVolume - (finance.tradeVolume + electronics.tradeVolume), total.tradeValue - (finance.tradeValue + electronics.tradeValue)); data.push(nonFinance, nonElectronics, nonFinanceNonElectronics); return symbol ? data.find(data => data.symbol === symbol) : data; } async fetchMarketTrades(options) { const { date } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?${query}`; const response = await this.httpService.get(url, { headers: { 'Connection': 'keep-alive' } }); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const [_, ...values] = json.tables[6].data.slice(-1)[0]; const data = {}; data.date = date, data.exchange = enums_1.Exchange.TWSE; data.tradeVolume = numeral(values[1]).value(); data.tradeValue = numeral(values[0]).value(); data.transaction = numeral(values[2]).value(); return data; } async fetchMarketBreadth(options) { const { date } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?${query}`; const response = await this.httpService.get(url, { headers: { 'Connection': 'keep-alive' } }); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const raw = json.tables[7].data.map((row) => row[2]); const [up, limitUp] = raw[0].replace(')', '').split('('); const [down, limitDown] = raw[1].replace(')', '').split('('); const [unchanged, unmatched, notApplicable] = raw.slice(2); const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE, data.up = numeral(up).value(); data.limitUp = numeral(limitUp).value(); data.down = numeral(down).value(); data.limitDown = numeral(limitDown).value(); data.unchanged = numeral(unchanged).value(); data.unmatched = numeral(unmatched).value(); data.notApplicable = numeral(notApplicable).value(); return data; } async fetchMarketInstitutional(options) { const { date } = options; const query = new URLSearchParams({ dayDate: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), type: 'day', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/fund/BFI82U?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE, data.institutional = json.data.map((row) => ({ investor: row[0] === '合計' ? '三大法人' : row[0], totalBuy: numeral(row[1]).value(), totalSell: numeral(row[2]).value(), difference: numeral(row[3]).value(), })); return data; } async fetchMarketMarginTrades(options) { const { date } = options; const query = new URLSearchParams({ date: luxon_1.DateTime.fromISO(date).toFormat('yyyyMMdd'), selectType: 'MS', response: 'json', }); const url = `https://www.twse.com.tw/rwd/zh/marginTrading/MI_MARGN?${query}`; const response = await this.httpService.get(url); const json = (response.data.stat === 'OK') && response.data; if (!json) return null; const values = json.tables[0].data.map((row) => row.slice(1)).flat(); const data = {}; data.date = date; data.exchange = enums_1.Exchange.TWSE, data.marginBuy = numeral(values[0]).value(); data.marginSell = numeral(values[1]).value(); data.marginRedeem = numeral(values[2]).value(); data.marginBalancePrev = numeral(values[3]).value(); data.marginBalance = numeral(values[4]).value(); data.shortBuy = numeral(values[5]).value(); data.shortSell = numeral(values[6]).value(); data.shortRedeem = numeral(values[7]).value(); data.shortBalancePrev = numeral(values[8]).value(); data.shortBalance = numeral(values[9]).value(); data.marginBuyValue = numeral(values[10]).value(); data.marginSellValue = numeral(values[11]).value(); data.marginRedeemValue = numeral(values[12]).value(); data.marginBalancePrevValue = numeral(values[13]).value(); data.marginBalanceValue = numeral(values[14]).value(); return data; } } exports.TwseScraper = TwseScraper;