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.
Files changed (135) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +168 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-dwdata-hub.umd.js +7 -0
  10. package/dist/w-dwdata-hub.umd.js.map +1 -0
  11. package/docs/WDwdataHub.mjs.html +136 -0
  12. package/docs/decodeEntities.mjs.html +129 -0
  13. package/docs/extractDivContent.mjs.html +147 -0
  14. package/docs/fetchAiNewsAggregator.mjs.html +155 -0
  15. package/docs/fetchAisixiang.mjs.html +1038 -0
  16. package/docs/fetchCnyes.mjs.html +260 -0
  17. package/docs/fetchGuancha.mjs.html +1007 -0
  18. package/docs/fetchHackerNews.mjs.html +189 -0
  19. package/docs/fetchMoneydj.mjs.html +251 -0
  20. package/docs/fetchMops.mjs.html +385 -0
  21. package/docs/fetchRSS.mjs.html +160 -0
  22. package/docs/fetchStatementdog.mjs.html +213 -0
  23. package/docs/fetchTaifex.mjs.html +513 -0
  24. package/docs/fetchTpex3insti.mjs.html +278 -0
  25. package/docs/fetchTpexMargin.mjs.html +248 -0
  26. package/docs/fetchTpexStock.mjs.html +199 -0
  27. package/docs/fetchTwDataHoliday.mjs.html +236 -0
  28. package/docs/fetchTwseMargin.mjs.html +253 -0
  29. package/docs/fetchTwseStock.mjs.html +188 -0
  30. package/docs/fetchTwseT86.mjs.html +208 -0
  31. package/docs/fetchWithRetry.mjs.html +376 -0
  32. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  37. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  38. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  39. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  47. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  48. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  49. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  50. package/docs/getOptFetch.mjs.html +185 -0
  51. package/docs/global.html +22649 -0
  52. package/docs/htmlToMarkdown.mjs.html +168 -0
  53. package/docs/index.html +84 -0
  54. package/docs/isYmd.mjs.html +114 -0
  55. package/docs/parseIntComma.mjs.html +100 -0
  56. package/docs/safeFilename.mjs.html +102 -0
  57. package/docs/scripts/collapse.js +39 -0
  58. package/docs/scripts/commonNav.js +28 -0
  59. package/docs/scripts/linenumber.js +25 -0
  60. package/docs/scripts/nav.js +12 -0
  61. package/docs/scripts/polyfill.js +4 -0
  62. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  63. package/docs/scripts/prettify/lang-css.js +2 -0
  64. package/docs/scripts/prettify/prettify.js +28 -0
  65. package/docs/scripts/search.js +99 -0
  66. package/docs/styles/jsdoc.css +776 -0
  67. package/docs/styles/prettify.css +80 -0
  68. package/docs/toDatetimeUTC8.mjs.html +143 -0
  69. package/docs/toRocDate.mjs.html +107 -0
  70. package/g.mjs +73 -0
  71. package/package.json +31 -0
  72. package/script.txt +17 -0
  73. package/src/WDwdataHub.mjs +64 -0
  74. package/src/decodeEntities.mjs +57 -0
  75. package/src/extractDivContent.mjs +75 -0
  76. package/src/fetchAiNewsAggregator.mjs +83 -0
  77. package/src/fetchAisixiang.mjs +966 -0
  78. package/src/fetchCnyes.mjs +188 -0
  79. package/src/fetchGuancha.mjs +935 -0
  80. package/src/fetchHackerNews.mjs +117 -0
  81. package/src/fetchMoneydj.mjs +179 -0
  82. package/src/fetchMops.mjs +313 -0
  83. package/src/fetchRSS.mjs +88 -0
  84. package/src/fetchStatementdog.mjs +141 -0
  85. package/src/fetchTaifex.mjs +441 -0
  86. package/src/fetchTpex3insti.mjs +206 -0
  87. package/src/fetchTpexMargin.mjs +176 -0
  88. package/src/fetchTpexStock.mjs +127 -0
  89. package/src/fetchTwDataHoliday.mjs +164 -0
  90. package/src/fetchTwseMargin.mjs +181 -0
  91. package/src/fetchTwseStock.mjs +116 -0
  92. package/src/fetchTwseT86.mjs +136 -0
  93. package/src/fetchWithRetry.mjs +304 -0
  94. package/src/getOptFetch.mjs +113 -0
  95. package/src/htmlToMarkdown.mjs +96 -0
  96. package/src/isYmd.mjs +42 -0
  97. package/src/parseIntComma.mjs +28 -0
  98. package/src/safeFilename.mjs +30 -0
  99. package/src/toDatetimeUTC8.mjs +71 -0
  100. package/src/toRocDate.mjs +35 -0
  101. package/test/WDwdataHub.test.mjs +63 -0
  102. package/test/decodeEntities.test.mjs +42 -0
  103. package/test/extractDivContent.test.mjs +48 -0
  104. package/test/fetchAiNewsAggregator.test.mjs +79 -0
  105. package/test/fetchAisixiang.test.mjs +225 -0
  106. package/test/fetchCnyes.test.mjs +61 -0
  107. package/test/fetchGuancha.test.mjs +190 -0
  108. package/test/fetchHackerNews.test.mjs +78 -0
  109. package/test/fetchMoneydj.test.mjs +82 -0
  110. package/test/fetchMops.test.mjs +106 -0
  111. package/test/fetchRSS.test.mjs +85 -0
  112. package/test/fetchStatementdog.test.mjs +69 -0
  113. package/test/fetchTaifex.test.mjs +99 -0
  114. package/test/fetchTpex3insti.test.mjs +90 -0
  115. package/test/fetchTpexMargin.test.mjs +94 -0
  116. package/test/fetchTpexStock.test.mjs +68 -0
  117. package/test/fetchTwDataHoliday.test.mjs +100 -0
  118. package/test/fetchTwseMargin.test.mjs +81 -0
  119. package/test/fetchTwseStock.test.mjs +80 -0
  120. package/test/fetchTwseT86.test.mjs +76 -0
  121. package/test/fetchWithRetry.test.mjs +174 -0
  122. package/test/htmlToMarkdown.test.mjs +71 -0
  123. package/test/isYmd.test.mjs +56 -0
  124. package/test/parseIntComma.test.mjs +38 -0
  125. package/test/safeFilename.test.mjs +35 -0
  126. package/test/toDatetimeUTC8.test.mjs +65 -0
  127. package/test/toRocDate.test.mjs +38 -0
  128. package/test/tools/encodeBig5.mjs +80 -0
  129. package/test/tools/fixtures.mjs +539 -0
  130. package/test/tools/hasChrome.mjs +39 -0
  131. package/test/tools/serverForTest.mjs +397 -0
  132. package/toolg/addVersion.mjs +4 -0
  133. package/toolg/cleanFolder.mjs +5 -0
  134. package/toolg/gDistRollup.mjs +49 -0
  135. 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 &lt; 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 === '"' &amp;&amp; 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 &lt; 2) {
146
+ return []
147
+ }
148
+
149
+ let headers = parseCSVLine(lines[0])
150
+ let rows = []
151
+ for (let i = 1; i &lt; lines.length; i++) {
152
+ let values = parseCSVLine(lines[i])
153
+ let obj = {}
154
+ for (let j = 0; j &lt; 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&amp;queryStartDate=${dateSlash}&amp;queryEndDate=${dateSlash}&amp;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' &amp;&amp; !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}&amp;queryEndDate=${dateSlash}&amp;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}&amp;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>