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,278 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>fetchTpex3insti.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">fetchTpex3insti.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 isearr from 'wsemi/src/isearr.mjs'
51
+ import fetchWithRetry from './fetchWithRetry.mjs'
52
+ import getOptFetch from './getOptFetch.mjs'
53
+ import isYmd from './isYmd.mjs'
54
+ import toRocDate from './toRocDate.mjs'
55
+
56
+
57
+ //櫃買中心網站根網址
58
+ let BASE_URL = 'https://www.tpex.org.tw'
59
+
60
+
61
+ //本函數之預設抓取設定
62
+ let DFLT = {
63
+ timeout: 15000,
64
+ maxRetries: 10,
65
+ baseDelayMs: 5000,
66
+ maxDelayMs: 30000,
67
+ label: 'fetch-tpex-3insti',
68
+ }
69
+
70
+
71
+ //TPEX 3insti_hedge端點之fields為「裸欄名」——買進股數/賣出股數/買賣超股數各重複7次
72
+ //(外資不含自營 / 外資自營 / 外資合計 / 投信 / 自營自行 / 自營避險 / 自營合計), 群組標籤不在JSON內
73
+ //故唯一可靠解法是以「固定位置」對應語意欄名(不可用欄名當key, 否則同名後者覆蓋前者、7組塌成1組)
74
+ //明細欄名對齊TWSE T86(外陸資.../投信.../自營商...), 令下游TWSE與TPEX共用同一套key
75
+ //末欄保留「三大法人買賣超股數合計」(下游有寫死讀此名者, 勿改)
76
+ let TPEX_FIELD_MAP = [
77
+ '代號',
78
+ '名稱',
79
+ '外陸資買進股數(不含外資自營商)',
80
+ '外陸資賣出股數(不含外資自營商)',
81
+ '外陸資買賣超股數(不含外資自營商)',
82
+ '外資自營商買進股數',
83
+ '外資自營商賣出股數',
84
+ '外資自營商買賣超股數',
85
+ '外資及陸資買進股數',
86
+ '外資及陸資賣出股數',
87
+ '外資及陸資買賣超股數',
88
+ '投信買進股數',
89
+ '投信賣出股數',
90
+ '投信買賣超股數',
91
+ '自營商買進股數(自行買賣)',
92
+ '自營商賣出股數(自行買賣)',
93
+ '自營商買賣超股數(自行買賣)',
94
+ '自營商買進股數(避險)',
95
+ '自營商賣出股數(避險)',
96
+ '自營商買賣超股數(避險)',
97
+ '自營商買進股數',
98
+ '自營商賣出股數',
99
+ '自營商買賣超股數',
100
+ '三大法人買賣超股數合計',
101
+ ]
102
+
103
+
104
+ /**
105
+ * 驗證TPEX回傳fields是否仍為預期結構
106
+ *
107
+ * 結構防呆(fail-loud), 驗證仍為「24欄、買進/賣出/買賣超循環×7」;
108
+ * 不符即拋錯(屬非暫時性錯誤故不重試), 避免API改版後靜默把外資的數字貼到投信頭上
109
+ *
110
+ * @param {Array} fields 輸入API回傳之欄名字串陣列
111
+ */
112
+ function assertTpexFieldShape(fields) {
113
+
114
+ if (fields.length !== TPEX_FIELD_MAP.length) {
115
+ throw new Error(`TPEX 3insti: 欄數異常,預期 ${TPEX_FIELD_MAP.length} 得 ${fields.length}(疑似 API 改版,請重新校準 TPEX_FIELD_MAP)`)
116
+ }
117
+ if (!String(fields[0]).includes('代號') || !String(fields[1]).includes('名稱')) {
118
+ throw new Error(`TPEX 3insti: 前兩欄非「代號/名稱」(得「${fields[0]}」「${fields[1]}」,疑似 API 改版)`)
119
+ }
120
+ if (!String(fields[fields.length - 1]).includes('合計')) {
121
+ throw new Error(`TPEX 3insti: 末欄非「合計」(得「${fields[fields.length - 1]}」,疑似 API 改版)`)
122
+ }
123
+
124
+ let cycle = ['買進股數', '賣出股數', '買賣超股數']
125
+ for (let i = 2; i &lt; fields.length - 1; i++) {
126
+ let expect = cycle[(i - 2) % 3]
127
+ if (String(fields[i]) !== expect) {
128
+ throw new Error(`TPEX 3insti: 第 ${i} 欄預期「${expect}」得「${fields[i]}」(疑似 API 改版,欄序已變)`)
129
+ }
130
+ }
131
+
132
+ }
133
+
134
+
135
+ /**
136
+ * 抓取櫃買中心(TPEX)上櫃三大法人買賣超
137
+ *
138
+ * 抓取指定日之上櫃三大法人買賣超(含自營商避險), 因API之fields為裸欄名且重複, 故以固定位置對應語意欄名;
139
+ * 套位置對應前先驗證欄位結構, 不符即拋錯以避免產生錯置之財務資料
140
+ *
141
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
142
+ * @param {Array} [stockCodes] 輸入股票代號字串陣列,省略或空陣列表示全市場
143
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
144
+ * @param {String} [opt.baseUrl='https://www.tpex.org.tw'] 輸入櫃買中心根網址字串,供測試或改指向鏡像時覆寫
145
+ * @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
146
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
147
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
148
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
149
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
150
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{source,date,data},日期無效、無資料或欄位結構不符時reject回傳錯誤物件
151
+ * @example
152
+ *
153
+ * import fetchTpex3insti from './src/fetchTpex3insti.mjs'
154
+ *
155
+ * let test = async () => {
156
+ *
157
+ * let r = await fetchTpex3insti('20260807', ['6488'])
158
+ * console.log(r.source, r.data.length, r.data[0]['代號'])
159
+ * // => 'tpex' 1 '6488'
160
+ *
161
+ * }
162
+ * await test()
163
+ * .catch((err) => {
164
+ * console.log(err)
165
+ * })
166
+ *
167
+ */
168
+ async function fetchTpex3insti(dateStr, stockCodes, opt = {}) {
169
+
170
+ //check
171
+ if (!isestr(dateStr) || !/^\d{8}$/.test(dateStr)) {
172
+ throw new Error(`dateStr must be YYYYMMDD, got: ${dateStr}`)
173
+ }
174
+ if (!isYmd(dateStr)) {
175
+ //合法性驗證, 例如20260230雖符合8碼但日期不存在
176
+ throw new Error(`dateStr 不是合法日期: ${dateStr}`)
177
+ }
178
+
179
+ //baseUrl
180
+ let baseUrl = get(opt, 'baseUrl')
181
+ if (!isestr(baseUrl)) {
182
+ baseUrl = BASE_URL
183
+ }
184
+ baseUrl = baseUrl.replace(/\/+$/, '')
185
+
186
+ //optFetch
187
+ let optFetch = getOptFetch(opt, DFLT)
188
+ let showLog = optFetch.showLog
189
+
190
+ //targetCodes
191
+ let targetCodes = isearr(stockCodes) ? stockCodes : []
192
+
193
+ //url
194
+ let rocDateStr = toRocDate(dateStr)
195
+ let url = `${baseUrl}/web/stock/3insti/daily_trade/3itrade_hedge_result.php?l=zh-tw&amp;t=D&amp;d=${rocDateStr}&amp;o=json`
196
+
197
+ if (showLog) {
198
+ console.log(`Fetching from: ${url}`)
199
+ console.log(`Target: ${targetCodes.length === 0 ? 'All Market' : targetCodes.join(', ')}`)
200
+ }
201
+
202
+ //data
203
+ let data = await fetchWithRetry(url, optFetch)
204
+
205
+ //check
206
+ if (!isearr(get(data, 'tables'))) {
207
+ throw new Error('TPEX 3insti: tables not found in response. Possibly a holiday or no data.')
208
+ }
209
+
210
+ //table, 此端點回tables[0]=資料表、tables[1]=空{}
211
+ //取含「代號」「名稱」表頭之資料表, 退而取tables[0], 避免未來新增其他資料表時誤抓
212
+ let tables = data.tables
213
+ let table = tables.find((t) => {
214
+ return Array.isArray(t.fields) &amp;&amp;
215
+ t.fields.some((f) => String(f).includes('代號')) &amp;&amp;
216
+ t.fields.some((f) => String(f).includes('名稱'))
217
+ }) || tables[0]
218
+
219
+ //fields, rawData
220
+ let fields = get(table, 'fields')
221
+ let rawData = get(table, 'data')
222
+ if (!Array.isArray(fields) || !Array.isArray(rawData)) {
223
+ throw new Error('TPEX 3insti: data/fields not found in table.')
224
+ }
225
+
226
+ //套位置對應前先驗結構(fields為裸欄名, 必須靠固定位置才能還原7組明細)
227
+ assertTpexFieldShape(fields)
228
+
229
+ //processedData
230
+ let processedData = rawData
231
+ .map((row) => {
232
+ let obj = {}
233
+ TPEX_FIELD_MAP.forEach((key, index) => {
234
+ let value = row[index]
235
+ if (typeof value === 'string') {
236
+ value = value.trim()
237
+ }
238
+ obj[key] = value
239
+ })
240
+ return obj
241
+ })
242
+ .filter((item) => targetCodes.length === 0 || targetCodes.includes(item['代號']))
243
+
244
+ if (showLog) {
245
+ console.log(`Fetched ${processedData.length} records.`)
246
+ }
247
+
248
+ return { source: 'tpex', date: dateStr, data: processedData }
249
+ }
250
+
251
+
252
+ export { TPEX_FIELD_MAP }
253
+ export default fetchTpex3insti
254
+ </code></pre>
255
+ </article>
256
+ </section>
257
+
258
+
259
+
260
+
261
+
262
+
263
+ </div>
264
+
265
+ <br class="clear">
266
+
267
+ <footer>
268
+ 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.
269
+ </footer>
270
+
271
+ <script>prettyPrint();</script>
272
+ <script src="scripts/polyfill.js"></script>
273
+ <script src="scripts/linenumber.js"></script>
274
+
275
+
276
+
277
+ </body>
278
+ </html>
@@ -0,0 +1,248 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>fetchTpexMargin.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">fetchTpexMargin.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 isearr from 'wsemi/src/isearr.mjs'
51
+ import fetchWithRetry from './fetchWithRetry.mjs'
52
+ import getOptFetch from './getOptFetch.mjs'
53
+ import isYmd from './isYmd.mjs'
54
+ import toRocDate from './toRocDate.mjs'
55
+ import parseIntComma from './parseIntComma.mjs'
56
+
57
+
58
+ //櫃買中心網站根網址
59
+ let BASE_URL = 'https://www.tpex.org.tw'
60
+
61
+
62
+ //本函數之預設抓取設定
63
+ let DFLT = {
64
+ timeout: 15000,
65
+ maxRetries: 10,
66
+ baseDelayMs: 5000,
67
+ maxDelayMs: 30000,
68
+ label: 'fetch-tpex-margin',
69
+ }
70
+
71
+
72
+ /**
73
+ * 抓取櫃買中心(TPEX)上櫃融資融券
74
+ *
75
+ * 抓取指定日之上櫃個股融資融券餘額並轉為結構化資料, 可指定股票代號陣列過濾;
76
+ * 選表以欄位同時含「資餘額」與「券餘額」比對, 失敗則退而取第一張有資料表, 並鎖定index6與index14兩錨點欄位驗證佈局,
77
+ * 不符即fail-loud, 避免固定index解析在API改版後靜默產生錯誤資料
78
+ *
79
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
80
+ * @param {Array} [stockCodes] 輸入股票代號字串陣列,省略或空陣列表示全市場
81
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
82
+ * @param {String} [opt.baseUrl='https://www.tpex.org.tw'] 輸入櫃買中心根網址字串,供測試或改指向鏡像時覆寫
83
+ * @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
84
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
85
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
86
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
87
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
88
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{source,date,count,data},日期無效、無資料、欄位佈局不符或指定個股查無資料時reject回傳錯誤物件
89
+ * @example
90
+ *
91
+ * import fetchTpexMargin from './src/fetchTpexMargin.mjs'
92
+ *
93
+ * let test = async () => {
94
+ *
95
+ * let r = await fetchTpexMargin('20260807', ['6488'])
96
+ * console.log(r.source, r.count, r.data[0].code)
97
+ * // => 'tpex_margin' 1 '6488'
98
+ *
99
+ * }
100
+ * await test()
101
+ * .catch((err) => {
102
+ * console.log(err)
103
+ * })
104
+ *
105
+ */
106
+ async function fetchTpexMargin(dateStr, stockCodes, opt = {}) {
107
+
108
+ //check
109
+ if (!isestr(dateStr) || !/^\d{8}$/.test(dateStr)) {
110
+ throw new Error(`日期參數無效:格式須為 YYYYMMDD (收到 "${dateStr}")`)
111
+ }
112
+ if (!isYmd(dateStr)) {
113
+ throw new Error(`日期參數無效:不合法的日期 (${dateStr})`)
114
+ }
115
+
116
+ //baseUrl
117
+ let baseUrl = get(opt, 'baseUrl')
118
+ if (!isestr(baseUrl)) {
119
+ baseUrl = BASE_URL
120
+ }
121
+ baseUrl = baseUrl.replace(/\/+$/, '')
122
+
123
+ //optFetch
124
+ let optFetch = getOptFetch(opt, DFLT)
125
+ let showLog = optFetch.showLog
126
+
127
+ //targetCodes
128
+ let targetCodes = isearr(stockCodes) ? stockCodes.filter(Boolean) : []
129
+
130
+ //url
131
+ let rocDate = toRocDate(dateStr)
132
+ let url = `${baseUrl}/web/stock/margin_trading/margin_balance/margin_bal_result.php?l=zh-tw&amp;d=${rocDate}&amp;o=json`
133
+
134
+ if (showLog) {
135
+ console.log(`Fetching TPEX margin data: ${dateStr} (${rocDate})`)
136
+ console.log(`Target: ${targetCodes.length > 0 ? targetCodes.join(', ') : 'All Market'}`)
137
+ console.log(`URL: ${url}`)
138
+ }
139
+
140
+ //data
141
+ let data = await fetchWithRetry(url, optFetch)
142
+
143
+ //marginTable, TPEX格式: { stat: 'ok', tables: [{ title, fields, data }] }
144
+ //fields: ["代號","名稱","前資餘額(張)","資買","資賣","現償","資餘額","資屬證金","資使用率(%)",
145
+ // "資限額","前券餘額(張)","券賣","券買","券償","券餘額","券屬證金","券使用率(%)","券限額","資券相抵(張)","備註"]
146
+ let tables = Array.isArray(data.tables) ? data.tables : []
147
+ let marginTable = tables.find((t) => {
148
+ return get(t, 'data.length', 0) > 0 &amp;&amp;
149
+ Array.isArray(t.fields) &amp;&amp;
150
+ t.fields.some((f) => String(f).includes('資餘額')) &amp;&amp;
151
+ t.fields.some((f) => String(f).includes('券餘額'))
152
+ }) || tables.find((t) => get(t, 'data.length', 0) > 0)
153
+ if (!marginTable || !isearr(get(marginTable, 'data'))) {
154
+ throw new Error('TPEX margin API returned no data. Possibly a holiday or data not yet available.')
155
+ }
156
+
157
+ //shape guard, 主比對失敗時會fallback取第一張有資料表, 若該表結構不同則下方固定index(2~19)會靜默解析錯欄位
158
+ //此處鎖定解析所依賴之兩個錨點欄位(index 6=資餘額、index 14=券餘額), 不符即fail-loud
159
+ let fields = Array.isArray(marginTable.fields) ? marginTable.fields : []
160
+ if (!String(fields[6] || '').includes('資餘額') || !String(fields[14] || '').includes('券餘額')) {
161
+ throw new Error(`TPEX 融資融券資料欄位佈局與預期不符(index6 應含「資餘額」實為「${fields[6] ?? ''}」、index14 應含「券餘額」實為「${fields[14] ?? ''}」);可能 API 格式變更,停止解析以免產生錯誤資料`)
162
+ }
163
+
164
+ //rows
165
+ let rows = marginTable.data
166
+ if (targetCodes.length > 0) {
167
+ rows = rows.filter((row) => targetCodes.includes(String(row[0] || '').trim()))
168
+ if (rows.length === 0) {
169
+ throw new Error(`指定個股 ${targetCodes.join(',')} 不在上櫃融資融券資料中(可能為上市股或代碼有誤)`)
170
+ }
171
+ }
172
+
173
+ //parsedData
174
+ //idx: 0=代號, 1=名稱, 2=前資餘額, 3=資買, 4=資賣, 5=現償, 6=資餘額,
175
+ // 7=資屬證金, 8=資使用率, 9=資限額, 10=前券餘額, 11=券賣, 12=券買,
176
+ // 13=券償, 14=券餘額, 15=券屬證金, 16=券使用率, 17=券限額, 18=資券相抵, 19=備註
177
+ let parsedData = rows.map((row) => {
178
+
179
+ let marginPrev = parseIntComma(row[2])
180
+ let marginBuy = parseIntComma(row[3])
181
+ let marginSell = parseIntComma(row[4])
182
+ let marginBalance = parseIntComma(row[6])
183
+ let shortPrev = parseIntComma(row[10])
184
+ let shortSell = parseIntComma(row[11])
185
+ let shortBuy = parseIntComma(row[12])
186
+ let shortBalance = parseIntComma(row[14])
187
+
188
+ return {
189
+ code: String(row[0] || '').trim(),
190
+ name: String(row[1] || '').trim(),
191
+ marginBuy,
192
+ marginSell,
193
+ marginCashRepay: parseIntComma(row[5]),
194
+ marginPrevBalance: marginPrev,
195
+ marginBalance,
196
+ marginChange: marginBalance - marginPrev,
197
+ marginLimit: parseIntComma(row[9]),
198
+ shortSell,
199
+ shortBuy,
200
+ shortCashRepay: parseIntComma(row[13]),
201
+ shortPrevBalance: shortPrev,
202
+ shortBalance,
203
+ shortChange: shortBalance - shortPrev,
204
+ shortLimit: parseIntComma(row[17]),
205
+ offset: parseIntComma(row[18]),
206
+ note: String(row[19] || '').trim(),
207
+ }
208
+ })
209
+
210
+ if (showLog) {
211
+ console.log(`Fetched ${parsedData.length} records.`)
212
+ }
213
+
214
+ return {
215
+ source: 'tpex_margin',
216
+ date: dateStr,
217
+ count: parsedData.length,
218
+ data: parsedData,
219
+ }
220
+ }
221
+
222
+
223
+ export default fetchTpexMargin
224
+ </code></pre>
225
+ </article>
226
+ </section>
227
+
228
+
229
+
230
+
231
+
232
+
233
+ </div>
234
+
235
+ <br class="clear">
236
+
237
+ <footer>
238
+ 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.
239
+ </footer>
240
+
241
+ <script>prettyPrint();</script>
242
+ <script src="scripts/polyfill.js"></script>
243
+ <script src="scripts/linenumber.js"></script>
244
+
245
+
246
+
247
+ </body>
248
+ </html>