w-dwdata-hub 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +55 -0
- package/.jsdoc +25 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +5 -0
- package/babel.config.js +16 -0
- package/dist/w-dwdata-hub.umd.js +7 -0
- package/dist/w-dwdata-hub.umd.js.map +1 -0
- package/docs/WDwdataHub.mjs.html +136 -0
- package/docs/decodeEntities.mjs.html +129 -0
- package/docs/extractDivContent.mjs.html +147 -0
- package/docs/fetchAiNewsAggregator.mjs.html +155 -0
- package/docs/fetchAisixiang.mjs.html +1038 -0
- package/docs/fetchCnyes.mjs.html +260 -0
- package/docs/fetchGuancha.mjs.html +1007 -0
- package/docs/fetchHackerNews.mjs.html +189 -0
- package/docs/fetchMoneydj.mjs.html +251 -0
- package/docs/fetchMops.mjs.html +385 -0
- package/docs/fetchRSS.mjs.html +160 -0
- package/docs/fetchStatementdog.mjs.html +213 -0
- package/docs/fetchTaifex.mjs.html +513 -0
- package/docs/fetchTpex3insti.mjs.html +278 -0
- package/docs/fetchTpexMargin.mjs.html +248 -0
- package/docs/fetchTpexStock.mjs.html +199 -0
- package/docs/fetchTwDataHoliday.mjs.html +236 -0
- package/docs/fetchTwseMargin.mjs.html +253 -0
- package/docs/fetchTwseStock.mjs.html +188 -0
- package/docs/fetchTwseT86.mjs.html +208 -0
- package/docs/fetchWithRetry.mjs.html +376 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/getOptFetch.mjs.html +185 -0
- package/docs/global.html +22649 -0
- package/docs/htmlToMarkdown.mjs.html +168 -0
- package/docs/index.html +84 -0
- package/docs/isYmd.mjs.html +114 -0
- package/docs/parseIntComma.mjs.html +100 -0
- package/docs/safeFilename.mjs.html +102 -0
- package/docs/scripts/collapse.js +39 -0
- package/docs/scripts/commonNav.js +28 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +99 -0
- package/docs/styles/jsdoc.css +776 -0
- package/docs/styles/prettify.css +80 -0
- package/docs/toDatetimeUTC8.mjs.html +143 -0
- package/docs/toRocDate.mjs.html +107 -0
- package/g.mjs +73 -0
- package/package.json +31 -0
- package/script.txt +17 -0
- package/src/WDwdataHub.mjs +64 -0
- package/src/decodeEntities.mjs +57 -0
- package/src/extractDivContent.mjs +75 -0
- package/src/fetchAiNewsAggregator.mjs +83 -0
- package/src/fetchAisixiang.mjs +966 -0
- package/src/fetchCnyes.mjs +188 -0
- package/src/fetchGuancha.mjs +935 -0
- package/src/fetchHackerNews.mjs +117 -0
- package/src/fetchMoneydj.mjs +179 -0
- package/src/fetchMops.mjs +313 -0
- package/src/fetchRSS.mjs +88 -0
- package/src/fetchStatementdog.mjs +141 -0
- package/src/fetchTaifex.mjs +441 -0
- package/src/fetchTpex3insti.mjs +206 -0
- package/src/fetchTpexMargin.mjs +176 -0
- package/src/fetchTpexStock.mjs +127 -0
- package/src/fetchTwDataHoliday.mjs +164 -0
- package/src/fetchTwseMargin.mjs +181 -0
- package/src/fetchTwseStock.mjs +116 -0
- package/src/fetchTwseT86.mjs +136 -0
- package/src/fetchWithRetry.mjs +304 -0
- package/src/getOptFetch.mjs +113 -0
- package/src/htmlToMarkdown.mjs +96 -0
- package/src/isYmd.mjs +42 -0
- package/src/parseIntComma.mjs +28 -0
- package/src/safeFilename.mjs +30 -0
- package/src/toDatetimeUTC8.mjs +71 -0
- package/src/toRocDate.mjs +35 -0
- package/test/WDwdataHub.test.mjs +63 -0
- package/test/decodeEntities.test.mjs +42 -0
- package/test/extractDivContent.test.mjs +48 -0
- package/test/fetchAiNewsAggregator.test.mjs +79 -0
- package/test/fetchAisixiang.test.mjs +225 -0
- package/test/fetchCnyes.test.mjs +61 -0
- package/test/fetchGuancha.test.mjs +190 -0
- package/test/fetchHackerNews.test.mjs +78 -0
- package/test/fetchMoneydj.test.mjs +82 -0
- package/test/fetchMops.test.mjs +106 -0
- package/test/fetchRSS.test.mjs +85 -0
- package/test/fetchStatementdog.test.mjs +69 -0
- package/test/fetchTaifex.test.mjs +99 -0
- package/test/fetchTpex3insti.test.mjs +90 -0
- package/test/fetchTpexMargin.test.mjs +94 -0
- package/test/fetchTpexStock.test.mjs +68 -0
- package/test/fetchTwDataHoliday.test.mjs +100 -0
- package/test/fetchTwseMargin.test.mjs +81 -0
- package/test/fetchTwseStock.test.mjs +80 -0
- package/test/fetchTwseT86.test.mjs +76 -0
- package/test/fetchWithRetry.test.mjs +174 -0
- package/test/htmlToMarkdown.test.mjs +71 -0
- package/test/isYmd.test.mjs +56 -0
- package/test/parseIntComma.test.mjs +38 -0
- package/test/safeFilename.test.mjs +35 -0
- package/test/toDatetimeUTC8.test.mjs +65 -0
- package/test/toRocDate.test.mjs +38 -0
- package/test/tools/encodeBig5.mjs +80 -0
- package/test/tools/fixtures.mjs +539 -0
- package/test/tools/hasChrome.mjs +39 -0
- package/test/tools/serverForTest.mjs +397 -0
- package/toolg/addVersion.mjs +4 -0
- package/toolg/cleanFolder.mjs +5 -0
- package/toolg/gDistRollup.mjs +49 -0
- package/toolg/modifyReadme.mjs +4 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>fetchTaifex.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#KNOWN_TOPICS">KNOWN_TOPICS</a></li><li><a href="global.html#WDwdataHub">WDwdataHub</a></li><li><a href="global.html#assertTpexFieldShape">assertTpexFieldShape</a></li><li><a href="global.html#decodeEntities">decodeEntities</a></li><li><a href="global.html#extractAuthorFromContent">extractAuthorFromContent</a></li><li><a href="global.html#extractDivContent">extractDivContent</a></li><li><a href="global.html#extractHtmlTitle">extractHtmlTitle</a></li><li><a href="global.html#extractPubTime">extractPubTime</a></li><li><a href="global.html#extractTitle">extractTitle</a></li><li><a href="global.html#fetchAiNewsAggregator">fetchAiNewsAggregator</a></li><li><a href="global.html#fetchAisixiang">fetchAisixiang</a></li><li><a href="global.html#fetchAllListPages">fetchAllListPages</a></li><li><a href="global.html#fetchAllSearchPages">fetchAllSearchPages</a></li><li><a href="global.html#fetchArticle">fetchArticle</a></li><li><a href="global.html#fetchAuthorArticles">fetchAuthorArticles</a></li><li><a href="global.html#fetchAuthorsList">fetchAuthorsList</a></li><li><a href="global.html#fetchCnyes">fetchCnyes</a></li><li><a href="global.html#fetchFuturesData">fetchFuturesData</a></li><li><a href="global.html#fetchGuancha">fetchGuancha</a></li><li><a href="global.html#fetchHackerNews">fetchHackerNews</a></li><li><a href="global.html#fetchHtml">fetchHtml</a></li><li><a href="global.html#fetchInstitutionalData">fetchInstitutionalData</a></li><li><a href="global.html#fetchKeywordArticles">fetchKeywordArticles</a></li><li><a href="global.html#fetchMoneydj">fetchMoneydj</a></li><li><a href="global.html#fetchMops">fetchMops</a></li><li><a href="global.html#fetchOnce">fetchOnce</a></li><li><a href="global.html#fetchPCRatio">fetchPCRatio</a></li><li><a href="global.html#fetchRSS">fetchRSS</a></li><li><a href="global.html#fetchStatementdog">fetchStatementdog</a></li><li><a href="global.html#fetchTaifex">fetchTaifex</a></li><li><a href="global.html#fetchTargetWithRetry">fetchTargetWithRetry</a></li><li><a href="global.html#fetchTitleArticles">fetchTitleArticles</a></li><li><a href="global.html#fetchTopicArticles">fetchTopicArticles</a></li><li><a href="global.html#fetchTopicsList">fetchTopicsList</a></li><li><a href="global.html#fetchTpex3insti">fetchTpex3insti</a></li><li><a href="global.html#fetchTpexMargin">fetchTpexMargin</a></li><li><a href="global.html#fetchTpexStock">fetchTpexStock</a></li><li><a href="global.html#fetchTwDataHoliday">fetchTwDataHoliday</a></li><li><a href="global.html#fetchTwseMargin">fetchTwseMargin</a></li><li><a href="global.html#fetchTwseStock">fetchTwseStock</a></li><li><a href="global.html#fetchTwseT86">fetchTwseT86</a></li><li><a href="global.html#fetchWithRetry">fetchWithRetry</a></li><li><a href="global.html#getCfg">getCfg</a></li><li><a href="global.html#getOptFetch">getOptFetch</a></li><li><a href="global.html#getPayload">getPayload</a></li><li><a href="global.html#htmlToMarkdown">htmlToMarkdown</a></li><li><a href="global.html#isHomepageHtml">isHomepageHtml</a></li><li><a href="global.html#isNonHolidayEntry">isNonHolidayEntry</a></li><li><a href="global.html#isRetryableError">isRetryableError</a></li><li><a href="global.html#isYmd">isYmd</a></li><li><a href="global.html#lookupAuthor">lookupAuthor</a></li><li><a href="global.html#lookupTopic">lookupTopic</a></li><li><a href="global.html#parseAuthorColumnPage">parseAuthorColumnPage</a></li><li><a href="global.html#parseAuthorIndex">parseAuthorIndex</a></li><li><a href="global.html#parseCSV">parseCSV</a></li><li><a href="global.html#parseCSVLine">parseCSVLine</a></li><li><a href="global.html#parseIntComma">parseIntComma</a></li><li><a href="global.html#parseListPage">parseListPage</a></li><li><a href="global.html#parseNum">parseNum</a></li><li><a href="global.html#parseSearchResults">parseSearchResults</a></li><li><a href="global.html#parseSearchTotalPages">parseSearchTotalPages</a></li><li><a href="global.html#parseZhuantiArticles">parseZhuantiArticles</a></li><li><a href="global.html#pickPint">pickPint</a></li><li><a href="global.html#rocToWestern">rocToWestern</a></li><li><a href="global.html#safeFilename">safeFilename</a></li><li><a href="global.html#toDatetimeUTC8">toDatetimeUTC8</a></li><li><a href="global.html#toRocDate">toRocDate</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">fetchTaifex.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
|
|
49
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
50
|
+
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
51
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
52
|
+
import delay from 'wsemi/src/delay.mjs'
|
|
53
|
+
import fetchWithRetry from './fetchWithRetry.mjs'
|
|
54
|
+
import getOptFetch from './getOptFetch.mjs'
|
|
55
|
+
import isYmd from './isYmd.mjs'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
//期交所網站根網址
|
|
59
|
+
let BASE_URL = 'https://www.taifex.com.tw'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
//本函數之預設抓取設定, 期交所CSV為MS950(Big5)編碼
|
|
63
|
+
let DFLT = {
|
|
64
|
+
responseType: 'text',
|
|
65
|
+
encoding: 'big5',
|
|
66
|
+
timeout: 15000,
|
|
67
|
+
maxRetries: 10,
|
|
68
|
+
baseDelayMs: 5000,
|
|
69
|
+
maxDelayMs: 30000,
|
|
70
|
+
label: 'fetch-taifex',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
//對期交所同網域請求改為序列執行, 每支間隔1秒, 降低被限流風險
|
|
75
|
+
let DEFAULT_INTER_REQUEST_DELAY_MS = 1000
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
//身份別中文名對應之結果key
|
|
79
|
+
let IDENTITY_MAP = {
|
|
80
|
+
'外資及陸資': 'foreign',
|
|
81
|
+
'外資': 'foreign',
|
|
82
|
+
'投信': 'trust',
|
|
83
|
+
'自營商': 'dealers',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 解析單行CSV
|
|
89
|
+
*
|
|
90
|
+
* 支援含逗號之引號欄位與""跳脫雙引號(RFC 4180子集, 足以涵蓋期交所未來欄位變動)
|
|
91
|
+
*
|
|
92
|
+
* @param {String} line 輸入CSV單行字串
|
|
93
|
+
* @returns {Array} 回傳欄位字串陣列
|
|
94
|
+
*/
|
|
95
|
+
function parseCSVLine(line) {
|
|
96
|
+
|
|
97
|
+
let result = []
|
|
98
|
+
let cur = ''
|
|
99
|
+
let inQuote = false
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < line.length; i++) {
|
|
102
|
+
let c = line[i]
|
|
103
|
+
if (inQuote) {
|
|
104
|
+
if (c === '"') {
|
|
105
|
+
if (line[i + 1] === '"') {
|
|
106
|
+
cur += '"'
|
|
107
|
+
i++
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
inQuote = false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
cur += c
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (c === ',') {
|
|
118
|
+
result.push(cur.trim())
|
|
119
|
+
cur = ''
|
|
120
|
+
}
|
|
121
|
+
else if (c === '"' && cur.length === 0) {
|
|
122
|
+
inQuote = true
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
cur += c
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
result.push(cur.trim())
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 解析CSV文字為物件陣列
|
|
136
|
+
*
|
|
137
|
+
* 以首列為欄名, 支援尾端逗號、引號欄位, 並去除前後空白
|
|
138
|
+
*
|
|
139
|
+
* @param {String} csvText 輸入CSV文字字串
|
|
140
|
+
* @returns {Array} 回傳物件陣列,資料不足2列時回傳[]
|
|
141
|
+
*/
|
|
142
|
+
function parseCSV(csvText) {
|
|
143
|
+
|
|
144
|
+
let lines = csvText.trim().split('\n').map((l) => l.trim()).filter((l) => l.length > 0)
|
|
145
|
+
if (lines.length < 2) {
|
|
146
|
+
return []
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let headers = parseCSVLine(lines[0])
|
|
150
|
+
let rows = []
|
|
151
|
+
for (let i = 1; i < lines.length; i++) {
|
|
152
|
+
let values = parseCSVLine(lines[i])
|
|
153
|
+
let obj = {}
|
|
154
|
+
for (let j = 0; j < headers.length; j++) {
|
|
155
|
+
obj[headers[j]] = values[j] || ''
|
|
156
|
+
}
|
|
157
|
+
rows.push(obj)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return rows
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 含千分位逗號之字串轉數值
|
|
166
|
+
*
|
|
167
|
+
* 與parseIntComma不同, 本函數保留小數並以null表達缺值, 供期交所價格與比率欄位使用
|
|
168
|
+
*
|
|
169
|
+
* @param {String} str 輸入數值字串
|
|
170
|
+
* @returns {Number|null} 回傳數值,為'-'、空字串或無法轉換時回傳null
|
|
171
|
+
*/
|
|
172
|
+
function parseNum(str) {
|
|
173
|
+
|
|
174
|
+
if (!str || str === '-' || str === '') {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let num = Number(str.replace(/,/g, ''))
|
|
179
|
+
|
|
180
|
+
return isNaN(num) ? null : num
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 抓取台指期行情
|
|
186
|
+
*
|
|
187
|
+
* @param {String} dateSlash 輸入日期YYYY/MM/DD字串
|
|
188
|
+
* @param {Object} cfg 輸入已正規化之設定物件
|
|
189
|
+
* @returns {Promise} 回傳Promise,resolve回傳近月合約行情物件
|
|
190
|
+
*/
|
|
191
|
+
async function fetchFuturesData(dateSlash, cfg) {
|
|
192
|
+
|
|
193
|
+
let url = `${cfg.baseUrl}/cht/3/futDataDown?down_type=1&queryStartDate=${dateSlash}&queryEndDate=${dateSlash}&commodity_id=TX`
|
|
194
|
+
if (cfg.showLog) {
|
|
195
|
+
console.log(`Fetching futures data: ${url}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let csvText = await fetchWithRetry(url, { ...cfg.optFetch, label: `${DFLT.label} Futures` })
|
|
199
|
+
let rows = parseCSV(csvText)
|
|
200
|
+
if (rows.length === 0) {
|
|
201
|
+
throw new Error('台指期行情:無資料(可能非交易日)')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//txRows, 排除到期月份含'/'之價差合約
|
|
205
|
+
let txRows = rows.filter((r) => r['契約'] === 'TX' && !r['到期月份(週別)'].includes('/'))
|
|
206
|
+
if (txRows.length === 0) {
|
|
207
|
+
throw new Error('台指期行情:找不到 TX 合約資料')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//依到期月份升冪排序以取近月
|
|
211
|
+
txRows.sort((a, b) => {
|
|
212
|
+
return a['到期月份(週別)'].trim().localeCompare(b['到期月份(週別)'].trim())
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
//nearMonth
|
|
216
|
+
let nearMonth = txRows[0]['到期月份(週別)'].trim()
|
|
217
|
+
let nearMonthRows = txRows.filter((r) => r['到期月份(週別)'].trim() === nearMonth)
|
|
218
|
+
|
|
219
|
+
//一般盤與盤後盤
|
|
220
|
+
let regularRow = nearMonthRows.find((r) => r['交易時段'] === '一般')
|
|
221
|
+
let afterHoursRow = nearMonthRows.find((r) => r['交易時段'] === '盤後')
|
|
222
|
+
|
|
223
|
+
let result = {
|
|
224
|
+
contractMonth: nearMonth,
|
|
225
|
+
open: null,
|
|
226
|
+
high: null,
|
|
227
|
+
low: null,
|
|
228
|
+
close: null,
|
|
229
|
+
settlement: null,
|
|
230
|
+
volume: null,
|
|
231
|
+
afterHoursClose: null,
|
|
232
|
+
afterHoursSettlement: null,
|
|
233
|
+
afterHoursVolume: null,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (regularRow) {
|
|
237
|
+
result.open = parseNum(regularRow['開盤價'])
|
|
238
|
+
result.high = parseNum(regularRow['最高價'])
|
|
239
|
+
result.low = parseNum(regularRow['最低價'])
|
|
240
|
+
result.close = parseNum(regularRow['收盤價'])
|
|
241
|
+
result.settlement = parseNum(regularRow['結算價'])
|
|
242
|
+
result.volume = parseNum(regularRow['成交量'])
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (afterHoursRow) {
|
|
246
|
+
result.afterHoursClose = parseNum(afterHoursRow['收盤價'])
|
|
247
|
+
result.afterHoursSettlement = parseNum(afterHoursRow['結算價'])
|
|
248
|
+
result.afterHoursVolume = parseNum(afterHoursRow['成交量'])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (cfg.showLog) {
|
|
252
|
+
console.log(` 台指期近月 (${nearMonth}): 開${result.open} 高${result.high} 低${result.low} 收${result.close} 結算${result.settlement} 量${result.volume}`)
|
|
253
|
+
if (result.afterHoursClose !== null) {
|
|
254
|
+
console.log(` 盤後: 收${result.afterHoursClose} 結算${result.afterHoursSettlement} 量${result.afterHoursVolume}`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 抓取三大法人期貨未平倉
|
|
264
|
+
*
|
|
265
|
+
* @param {String} dateSlash 輸入日期YYYY/MM/DD字串
|
|
266
|
+
* @param {Object} cfg 輸入已正規化之設定物件
|
|
267
|
+
* @returns {Promise} 回傳Promise,resolve回傳以foreign、trust、dealers為key之未平倉物件
|
|
268
|
+
*/
|
|
269
|
+
async function fetchInstitutionalData(dateSlash, cfg) {
|
|
270
|
+
|
|
271
|
+
let url = `${cfg.baseUrl}/cht/3/futContractsDateDown?queryStartDate=${dateSlash}&queryEndDate=${dateSlash}&commodityId=TXF`
|
|
272
|
+
if (cfg.showLog) {
|
|
273
|
+
console.log(`Fetching institutional data: ${url}`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let csvText = await fetchWithRetry(url, { ...cfg.optFetch, label: `${DFLT.label} Institutional` })
|
|
277
|
+
let rows = parseCSV(csvText)
|
|
278
|
+
if (rows.length === 0) {
|
|
279
|
+
throw new Error('三大法人期貨未平倉:無資料(可能非交易日)')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let result = {}
|
|
283
|
+
for (let row of rows) {
|
|
284
|
+
|
|
285
|
+
let identity = row['身份別']
|
|
286
|
+
let key = IDENTITY_MAP[identity]
|
|
287
|
+
if (!key) {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
result[key] = {
|
|
292
|
+
longContracts: parseNum(row['多方未平倉口數']),
|
|
293
|
+
longAmount: parseNum(row['多方未平倉契約金額(千元)']),
|
|
294
|
+
shortContracts: parseNum(row['空方未平倉口數']),
|
|
295
|
+
shortAmount: parseNum(row['空方未平倉契約金額(千元)']),
|
|
296
|
+
netContracts: parseNum(row['多空未平倉口數淨額']),
|
|
297
|
+
netAmount: parseNum(row['多空未平倉契約金額淨額(千元)']),
|
|
298
|
+
tradingLong: parseNum(row['多方交易口數']),
|
|
299
|
+
tradingShort: parseNum(row['空方交易口數']),
|
|
300
|
+
tradingNet: parseNum(row['多空交易口數淨額']),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (cfg.showLog) {
|
|
304
|
+
console.log(` ${identity}: 未平倉淨額 ${result[key].netContracts} 口`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//rows非空但無任一身份別命中(例如官方變更欄位用語), 視為解析失敗
|
|
310
|
+
//比照本檔其他解析在資料缺漏時拋錯之慣例, 避免回傳空物件被當成成功而靜默缺資料
|
|
311
|
+
if (Object.keys(result).length === 0) {
|
|
312
|
+
throw new Error('三大法人期貨未平倉:無法解析身份別(可能官方變更欄位用語)')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 抓取Put/Call Ratio
|
|
321
|
+
*
|
|
322
|
+
* @param {String} dateSlash 輸入日期YYYY/MM/DD字串
|
|
323
|
+
* @param {Object} cfg 輸入已正規化之設定物件
|
|
324
|
+
* @returns {Promise} 回傳Promise,resolve回傳Put/Call Ratio物件
|
|
325
|
+
*/
|
|
326
|
+
async function fetchPCRatio(dateSlash, cfg) {
|
|
327
|
+
|
|
328
|
+
let url = `${cfg.baseUrl}/cht/3/pcRatioDown?queryStartDate=${dateSlash}&queryEndDate=${dateSlash}`
|
|
329
|
+
if (cfg.showLog) {
|
|
330
|
+
console.log(`Fetching P/C ratio: ${url}`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let csvText = await fetchWithRetry(url, { ...cfg.optFetch, label: `${DFLT.label} PCRatio` })
|
|
334
|
+
let rows = parseCSV(csvText)
|
|
335
|
+
if (rows.length === 0) {
|
|
336
|
+
throw new Error('Put/Call Ratio:無資料(可能非交易日)')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let row = rows[0]
|
|
340
|
+
let result = {
|
|
341
|
+
putVolume: parseNum(row['賣權成交量']),
|
|
342
|
+
callVolume: parseNum(row['買權成交量']),
|
|
343
|
+
ratio: parseNum(row['買賣權成交量比率%']),
|
|
344
|
+
putOpenInterest: parseNum(row['賣權未平倉量']),
|
|
345
|
+
callOpenInterest: parseNum(row['買權未平倉量']),
|
|
346
|
+
openInterestRatio: parseNum(row['買賣權未平倉量比率%']),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (cfg.showLog) {
|
|
350
|
+
console.log(` Put ${result.putVolume} / Call ${result.callVolume} = ${result.ratio}%`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 抓取期交所(TAIFEX)台指期相關資料
|
|
359
|
+
*
|
|
360
|
+
* 序列抓取台指期行情、三大法人期貨未平倉與Put/Call Ratio共三支CSV(每支間隔1秒以降低限流風險);
|
|
361
|
+
* 期交所CSV為Big5(MS950)編碼, 由本函數解碼; 三支之中任一支失敗僅記錄於errors, 三支全失敗才拋錯
|
|
362
|
+
*
|
|
363
|
+
* @param {String} dateStr 輸入日期YYYYMMDD字串
|
|
364
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
365
|
+
* @param {String} [opt.baseUrl='https://www.taifex.com.tw'] 輸入期交所根網址字串,供測試或改指向鏡像時覆寫
|
|
366
|
+
* @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
|
|
367
|
+
* @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
|
|
368
|
+
* @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
|
|
369
|
+
* @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
|
|
370
|
+
* @param {Integer} [opt.interRequestDelayMs=1000] 輸入三支CSV之間隔毫秒整數,預設1000
|
|
371
|
+
* @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
|
|
372
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件{date,futures,institutional,pcRatio,errors},日期無效或三支資料全失敗時reject回傳錯誤物件
|
|
373
|
+
* @example
|
|
374
|
+
*
|
|
375
|
+
* import fetchTaifex from './src/fetchTaifex.mjs'
|
|
376
|
+
*
|
|
377
|
+
* let test = async () => {
|
|
378
|
+
*
|
|
379
|
+
* let r = await fetchTaifex('20260807')
|
|
380
|
+
* console.log(r.date, r.futures.tx.close, r.pcRatio.ratio, r.errors)
|
|
381
|
+
* // => '20260807' 23456 108.5 []
|
|
382
|
+
*
|
|
383
|
+
* }
|
|
384
|
+
* await test()
|
|
385
|
+
* .catch((err) => {
|
|
386
|
+
* console.log(err)
|
|
387
|
+
* })
|
|
388
|
+
*
|
|
389
|
+
*/
|
|
390
|
+
async function fetchTaifex(dateStr, opt = {}) {
|
|
391
|
+
|
|
392
|
+
//check, 函數入口驗日期
|
|
393
|
+
if (!isestr(dateStr) || !/^\d{8}$/.test(dateStr)) {
|
|
394
|
+
throw new Error(`dateStr 須為 YYYYMMDD 字串,得到: ${dateStr}`)
|
|
395
|
+
}
|
|
396
|
+
if (!isYmd(dateStr)) {
|
|
397
|
+
throw new Error(`dateStr 非合法日期: ${dateStr}`)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
//baseUrl
|
|
401
|
+
let baseUrl = get(opt, 'baseUrl')
|
|
402
|
+
if (!isestr(baseUrl)) {
|
|
403
|
+
baseUrl = BASE_URL
|
|
404
|
+
}
|
|
405
|
+
baseUrl = baseUrl.replace(/\/+$/, '')
|
|
406
|
+
|
|
407
|
+
//cfg
|
|
408
|
+
let optFetch = getOptFetch(opt, DFLT)
|
|
409
|
+
let showLog = optFetch.showLog
|
|
410
|
+
let cfg = { baseUrl, showLog, optFetch }
|
|
411
|
+
|
|
412
|
+
//interRequestDelayMs
|
|
413
|
+
let interRequestDelayMs = get(opt, 'interRequestDelayMs')
|
|
414
|
+
if (!isp0int(interRequestDelayMs)) {
|
|
415
|
+
interRequestDelayMs = DEFAULT_INTER_REQUEST_DELAY_MS
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
interRequestDelayMs = cint(interRequestDelayMs)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
//dateSlash
|
|
422
|
+
let dateSlash = `${dateStr.substring(0, 4)}/${dateStr.substring(4, 6)}/${dateStr.substring(6, 8)}`
|
|
423
|
+
if (showLog) {
|
|
424
|
+
console.log(`Fetching TAIFEX data for ${dateStr} (${dateSlash})`)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let errors = []
|
|
428
|
+
let futures = null
|
|
429
|
+
let institutional = null
|
|
430
|
+
let pcRatio = null
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
futures = await fetchFuturesData(dateSlash, cfg)
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
let msg = `台指期行情: ${get(err, 'message', err)}`
|
|
437
|
+
if (showLog) {
|
|
438
|
+
console.error(msg)
|
|
439
|
+
}
|
|
440
|
+
errors.push(msg)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await delay(interRequestDelayMs)
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
institutional = await fetchInstitutionalData(dateSlash, cfg)
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
let msg = `三大法人: ${get(err, 'message', err)}`
|
|
450
|
+
if (showLog) {
|
|
451
|
+
console.error(msg)
|
|
452
|
+
}
|
|
453
|
+
errors.push(msg)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await delay(interRequestDelayMs)
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
pcRatio = await fetchPCRatio(dateSlash, cfg)
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
let msg = `Put/Call Ratio: ${get(err, 'message', err)}`
|
|
463
|
+
if (showLog) {
|
|
464
|
+
console.error(msg)
|
|
465
|
+
}
|
|
466
|
+
errors.push(msg)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
//三支全失敗才視為完全失敗
|
|
470
|
+
let hasAnyData = futures || institutional || pcRatio
|
|
471
|
+
if (!hasAnyData) {
|
|
472
|
+
let err = new Error(`所有資料抓取失敗: ${errors.join('; ')}`)
|
|
473
|
+
err.errors = errors
|
|
474
|
+
throw err
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
date: dateStr,
|
|
479
|
+
futures: futures ? { tx: futures } : null,
|
|
480
|
+
institutional: institutional || null,
|
|
481
|
+
pcRatio: pcRatio || null,
|
|
482
|
+
errors,
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
export { parseCSV, parseCSVLine, parseNum }
|
|
488
|
+
export default fetchTaifex
|
|
489
|
+
</code></pre>
|
|
490
|
+
</article>
|
|
491
|
+
</section>
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<br class="clear">
|
|
501
|
+
|
|
502
|
+
<footer>
|
|
503
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 07 2026 12:04:51 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
504
|
+
</footer>
|
|
505
|
+
|
|
506
|
+
<script>prettyPrint();</script>
|
|
507
|
+
<script src="scripts/polyfill.js"></script>
|
|
508
|
+
<script src="scripts/linenumber.js"></script>
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
</body>
|
|
513
|
+
</html>
|