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,181 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+ import isearr from 'wsemi/src/isearr.mjs'
4
+ import fetchWithRetry from './fetchWithRetry.mjs'
5
+ import getOptFetch from './getOptFetch.mjs'
6
+ import isYmd from './isYmd.mjs'
7
+ import parseIntComma from './parseIntComma.mjs'
8
+
9
+
10
+ //證交所網站根網址
11
+ let BASE_URL = 'https://www.twse.com.tw'
12
+
13
+
14
+ //本函數之預設抓取設定
15
+ let DFLT = {
16
+ timeout: 15000,
17
+ maxRetries: 10,
18
+ baseDelayMs: 5000,
19
+ maxDelayMs: 30000,
20
+ label: 'fetch-twse-margin',
21
+ }
22
+
23
+
24
+ //融資融券彙總表之文件化欄數, 下方固定index解析讀至row[15], 故欄數不足即視為API改版
25
+ let MIN_FIELDS = 16
26
+
27
+
28
+ /**
29
+ * 抓取證交所(TWSE)上市融資融券
30
+ *
31
+ * 抓取指定日之上市個股融資融券餘額並轉為結構化資料, 可指定股票代號陣列過濾;
32
+ * 選表先以標題「融資融券彙總」精確比對, 失敗則退而以欄位同時含「融資」與「融券」比對, 提升schema改版容忍度
33
+ *
34
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
35
+ * @param {Array} [stockCodes] 輸入股票代號字串陣列,省略或空陣列表示全市場
36
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
37
+ * @param {String} [opt.baseUrl='https://www.twse.com.tw'] 輸入證交所根網址字串,供測試或改指向鏡像時覆寫
38
+ * @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
39
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
40
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
41
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
42
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
43
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{source,date,count,data},日期無效、API錯誤或指定個股查無資料時reject回傳錯誤物件
44
+ * @example
45
+ *
46
+ * import fetchTwseMargin from './src/fetchTwseMargin.mjs'
47
+ *
48
+ * let test = async () => {
49
+ *
50
+ * let r = await fetchTwseMargin('20260807', ['2330'])
51
+ * console.log(r.source, r.count, r.data[0].code, r.data[0].marginBalance)
52
+ * // => 'twse_margin' 1 '2330' 12345
53
+ *
54
+ * }
55
+ * await test()
56
+ * .catch((err) => {
57
+ * console.log(err)
58
+ * })
59
+ *
60
+ */
61
+ async function fetchTwseMargin(dateStr, stockCodes, opt = {}) {
62
+
63
+ //check
64
+ if (!isestr(dateStr) || !/^\d{8}$/.test(dateStr)) {
65
+ throw new Error(`日期參數無效:格式須為 YYYYMMDD (收到 "${dateStr}")`)
66
+ }
67
+ if (!isYmd(dateStr)) {
68
+ throw new Error(`日期參數無效:不合法的日期 (${dateStr})`)
69
+ }
70
+
71
+ //baseUrl
72
+ let baseUrl = get(opt, 'baseUrl')
73
+ if (!isestr(baseUrl)) {
74
+ baseUrl = BASE_URL
75
+ }
76
+ baseUrl = baseUrl.replace(/\/+$/, '')
77
+
78
+ //optFetch
79
+ let optFetch = getOptFetch(opt, DFLT)
80
+ let showLog = optFetch.showLog
81
+
82
+ //targetCodes
83
+ let targetCodes = isearr(stockCodes) ? stockCodes.filter(Boolean) : []
84
+
85
+ //url
86
+ let url = `${baseUrl}/rwd/zh/marginTrading/MI_MARGN?date=${dateStr}&selectType=ALL&response=json`
87
+
88
+ if (showLog) {
89
+ console.log(`Fetching TWSE margin data: ${dateStr}`)
90
+ console.log(`Target: ${targetCodes.length > 0 ? targetCodes.join(', ') : 'All Market'}`)
91
+ console.log(`URL: ${url}`)
92
+ }
93
+
94
+ //data
95
+ let data = await fetchWithRetry(url, optFetch)
96
+
97
+ //check
98
+ if (get(data, 'stat') !== 'OK') {
99
+ throw new Error(`TWSE MI_MARGN API returned: ${get(data, 'stat')}`)
100
+ }
101
+
102
+ //detailTable, 先以title精確比對, 失敗則退而以fields雙重比對
103
+ //fields: ["代號","名稱","買進","賣出","現金償還","前日餘額","今日餘額","次一營業日限額",
104
+ // "買進","賣出","現券償還","前日餘額","今日餘額","次一營業日限額","資券互抵","註記"]
105
+ //前8欄為融資, idx 8-13為融券, idx 14=資券互抵, idx 15=註記
106
+ let tables = Array.isArray(data.tables) ? data.tables : []
107
+ let detailTable = tables.find((t) => get(t, 'data.length', 0) > 0 && get(t, 'title', '').includes('融資融券彙總'))
108
+ if (!detailTable) {
109
+ detailTable = tables.find((t) => {
110
+ return get(t, 'data.length', 0) > 0 &&
111
+ Array.isArray(t.fields) &&
112
+ t.fields.some((f) => String(f).includes('融資')) &&
113
+ t.fields.some((f) => String(f).includes('融券'))
114
+ })
115
+ }
116
+ if (!detailTable || !isearr(get(detailTable, 'data'))) {
117
+ throw new Error('TWSE MI_MARGN: 找不到融資融券彙總資料表')
118
+ }
119
+
120
+ //shape sanity, 固定index解析讀至row[15], 驗證欄數避免fallback抓到結構不同的表後靜默解析錯欄
121
+ if (!Array.isArray(detailTable.fields) || detailTable.fields.length < MIN_FIELDS) {
122
+ throw new Error(`TWSE 融資融券資料欄數不符預期(fields ${get(detailTable, 'fields.length', 'N/A')} 欄,應 >= ${MIN_FIELDS}),可能 API 格式變更`)
123
+ }
124
+
125
+ //rows
126
+ let rows = detailTable.data
127
+ if (targetCodes.length > 0) {
128
+ rows = rows.filter((row) => targetCodes.includes(String(row[0] || '').trim()))
129
+ if (rows.length === 0) {
130
+ throw new Error(`指定個股 ${targetCodes.join(',')} 不在上市融資融券資料中(可能為上櫃股或代碼有誤)`)
131
+ }
132
+ }
133
+
134
+ //parsedData
135
+ let parsedData = rows.map((row) => {
136
+
137
+ let marginBuy = parseIntComma(row[2])
138
+ let marginSell = parseIntComma(row[3])
139
+ let marginBalance = parseIntComma(row[6])
140
+ let marginPrev = parseIntComma(row[5])
141
+ let shortBuy = parseIntComma(row[8]) //融券買進(回補)
142
+ let shortSell = parseIntComma(row[9]) //融券賣出(新增放空)
143
+ let shortBalance = parseIntComma(row[12])
144
+ let shortPrev = parseIntComma(row[11])
145
+
146
+ return {
147
+ code: String(row[0] || '').trim(),
148
+ name: String(row[1] || '').trim(),
149
+ marginBuy,
150
+ marginSell,
151
+ marginCashRepay: parseIntComma(row[4]),
152
+ marginPrevBalance: marginPrev,
153
+ marginBalance,
154
+ marginChange: marginBalance - marginPrev,
155
+ marginLimit: parseIntComma(row[7]),
156
+ shortSell,
157
+ shortBuy,
158
+ shortCashRepay: parseIntComma(row[10]),
159
+ shortPrevBalance: shortPrev,
160
+ shortBalance,
161
+ shortChange: shortBalance - shortPrev,
162
+ shortLimit: parseIntComma(row[13]),
163
+ offset: parseIntComma(row[14]),
164
+ note: String(row[15] || '').trim(),
165
+ }
166
+ })
167
+
168
+ if (showLog) {
169
+ console.log(`Fetched ${parsedData.length} records.`)
170
+ }
171
+
172
+ return {
173
+ source: 'twse_margin',
174
+ date: dateStr,
175
+ count: parsedData.length,
176
+ data: parsedData,
177
+ }
178
+ }
179
+
180
+
181
+ export default fetchTwseMargin
@@ -0,0 +1,116 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+ import fetchWithRetry from './fetchWithRetry.mjs'
4
+ import getOptFetch from './getOptFetch.mjs'
5
+ import isYmd from './isYmd.mjs'
6
+ import toRocDate from './toRocDate.mjs'
7
+
8
+
9
+ //證交所網站根網址
10
+ let BASE_URL = 'https://www.twse.com.tw'
11
+
12
+
13
+ //本函數之預設抓取設定
14
+ let DFLT = {
15
+ timeout: 10000,
16
+ maxRetries: 10,
17
+ baseDelayMs: 5000,
18
+ maxDelayMs: 30000,
19
+ label: 'fetch-twse-stock',
20
+ }
21
+
22
+
23
+ /**
24
+ * 抓取證交所(TWSE)上市股價
25
+ *
26
+ * 指定stockCode時走STOCK_DAY取個股日成交資訊(該API回傳整月資料, 本函數過濾為指定日單筆);
27
+ * 未指定或指定為'all'時走MI_INDEX取全市場收盤資料
28
+ *
29
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
30
+ * @param {String} [stockCode] 輸入股票代號字串,例如'2330',省略或給'all'表示全市場
31
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
32
+ * @param {String} [opt.baseUrl='https://www.twse.com.tw'] 輸入證交所根網址字串,供測試或改指向鏡像時覆寫
33
+ * @param {Integer} [opt.timeout=10000] 輸入單次請求逾時毫秒整數,預設10000
34
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
35
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
36
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
37
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
38
+ * @returns {Promise} 回傳Promise,resolve回傳證交所API原始資料物件,內含fields與data等欄位,日期無效、API回傳非OK或指定日無交易資料時reject回傳錯誤物件
39
+ * @example
40
+ *
41
+ * import fetchTwseStock from './src/fetchTwseStock.mjs'
42
+ *
43
+ * let test = async () => {
44
+ *
45
+ * let r = await fetchTwseStock('20260807', '2330')
46
+ * console.log(r.stat, r.data.length)
47
+ * // => 'OK' 1
48
+ *
49
+ * }
50
+ * await test()
51
+ * .catch((err) => {
52
+ * console.log(err)
53
+ * })
54
+ *
55
+ */
56
+ async function fetchTwseStock(dateStr, stockCode, opt = {}) {
57
+
58
+ //check, 函數入口驗日期, 令程式化呼叫不繞過CLI之檢核
59
+ if (!isYmd(dateStr)) {
60
+ throw new Error(`dateStr 須為合法之 YYYYMMDD 字串,得到: ${dateStr}`)
61
+ }
62
+
63
+ //baseUrl
64
+ let baseUrl = get(opt, 'baseUrl')
65
+ if (!isestr(baseUrl)) {
66
+ baseUrl = BASE_URL
67
+ }
68
+ baseUrl = baseUrl.replace(/\/+$/, '')
69
+
70
+ //optFetch
71
+ let optFetch = getOptFetch(opt, DFLT)
72
+ let showLog = optFetch.showLog
73
+
74
+ //isSingleStock
75
+ let isSingleStock = isestr(stockCode) && stockCode.toLowerCase() !== 'all'
76
+ let stockNo = isSingleStock ? stockCode : 'ALLBUT0999'
77
+
78
+ //url
79
+ let url = ''
80
+ if (isSingleStock) {
81
+ url = `${baseUrl}/exchangeReport/STOCK_DAY?response=json&date=${dateStr}&stockNo=${stockNo}`
82
+ }
83
+ else {
84
+ url = `${baseUrl}/exchangeReport/MI_INDEX?response=json&date=${dateStr}&type=ALLBUT0999`
85
+ }
86
+
87
+ if (showLog) {
88
+ console.log(`Fetching TWSE data: ${dateStr}, Stock: ${stockNo}`)
89
+ console.log(`URL: ${url}`)
90
+ }
91
+
92
+ //data
93
+ let data = await fetchWithRetry(url, optFetch)
94
+
95
+ //check
96
+ if (get(data, 'stat') !== 'OK') {
97
+ throw new Error(`TWSE API returned: ${get(data, 'stat')}`)
98
+ }
99
+
100
+ //STOCK_DAY回傳整月資料, 呼叫者既已指定單一日期則過濾為該日單筆
101
+ //保留原欄位結構(fields/data/title等), 僅替換data為篩選後陣列
102
+ if (isSingleStock && Array.isArray(data.data)) {
103
+ let rocDate = toRocDate(dateStr)
104
+ let filtered = data.data.filter((row) => row[0] === rocDate)
105
+ data.data = filtered
106
+ if (filtered.length === 0) {
107
+ //整月有資料但指定日無 → 當日停盤、假日或未開市
108
+ throw new Error(`TWSE 個股 ${stockCode} 於 ${dateStr} 無交易資料(可能為假日或停盤)`)
109
+ }
110
+ }
111
+
112
+ return data
113
+ }
114
+
115
+
116
+ export default fetchTwseStock
@@ -0,0 +1,136 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+ import isearr from 'wsemi/src/isearr.mjs'
4
+ import fetchWithRetry from './fetchWithRetry.mjs'
5
+ import getOptFetch from './getOptFetch.mjs'
6
+ import isYmd from './isYmd.mjs'
7
+
8
+
9
+ //證交所網站根網址
10
+ let BASE_URL = 'https://www.twse.com.tw'
11
+
12
+
13
+ //本函數之預設抓取設定
14
+ let DFLT = {
15
+ timeout: 15000,
16
+ maxRetries: 10,
17
+ baseDelayMs: 5000,
18
+ maxDelayMs: 30000,
19
+ label: 'fetch-twse-t86',
20
+ }
21
+
22
+
23
+ /**
24
+ * 抓取證交所(TWSE)上市三大法人買賣超
25
+ *
26
+ * 抓取指定日之上市三大法人買賣超(T86), 以API回傳之fields為key轉為物件陣列, 可指定股票代號陣列過濾
27
+ *
28
+ * @param {String} dateStr 輸入日期YYYYMMDD字串
29
+ * @param {Array} [stockCodes] 輸入股票代號字串陣列,省略或空陣列表示全市場
30
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
31
+ * @param {String} [opt.baseUrl='https://www.twse.com.tw'] 輸入證交所根網址字串,供測試或改指向鏡像時覆寫
32
+ * @param {Integer} [opt.timeout=15000] 輸入單次請求逾時毫秒整數,預設15000
33
+ * @param {Integer} [opt.maxRetries=10] 輸入最大重試次數整數,含初始共執行maxRetries+1次,預設10
34
+ * @param {Integer} [opt.baseDelayMs=5000] 輸入重試之線性退避基礎毫秒整數,預設5000
35
+ * @param {Integer} [opt.maxDelayMs=30000] 輸入重試之線性退避上限毫秒整數,預設30000
36
+ * @param {Boolean} [opt.showLog=true] 輸入是否顯示過程訊息布林值,預設true
37
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{source,date,data},日期無效或API錯誤時reject回傳錯誤物件
38
+ * @example
39
+ *
40
+ * import fetchTwseT86 from './src/fetchTwseT86.mjs'
41
+ *
42
+ * let test = async () => {
43
+ *
44
+ * let r = await fetchTwseT86('20260807', ['2330'])
45
+ * console.log(r.source, r.data.length, r.data[0]['證券代號'])
46
+ * // => 'twse' 1 '2330'
47
+ *
48
+ * }
49
+ * await test()
50
+ * .catch((err) => {
51
+ * console.log(err)
52
+ * })
53
+ *
54
+ */
55
+ async function fetchTwseT86(dateStr, stockCodes, opt = {}) {
56
+
57
+ //check
58
+ if (!isestr(dateStr) || !/^\d{8}$/.test(dateStr)) {
59
+ throw new Error(`dateStr must be YYYYMMDD, got: ${dateStr}`)
60
+ }
61
+ if (!isYmd(dateStr)) {
62
+ //合法性驗證, 例如20260230雖然符合8碼但日期不存在
63
+ throw new Error(`dateStr 不是合法日期: ${dateStr}`)
64
+ }
65
+
66
+ //baseUrl
67
+ let baseUrl = get(opt, 'baseUrl')
68
+ if (!isestr(baseUrl)) {
69
+ baseUrl = BASE_URL
70
+ }
71
+ baseUrl = baseUrl.replace(/\/+$/, '')
72
+
73
+ //optFetch
74
+ let optFetch = getOptFetch(opt, DFLT)
75
+ let showLog = optFetch.showLog
76
+
77
+ //targetCodes
78
+ let targetCodes = isearr(stockCodes) ? stockCodes : []
79
+
80
+ //url
81
+ let url = `${baseUrl}/rwd/zh/fund/T86?response=json&date=${dateStr}&selectType=ALL`
82
+
83
+ if (showLog) {
84
+ console.log(`Fetching from: ${url}`)
85
+ console.log(`Target: ${targetCodes.length === 0 ? 'All Market' : targetCodes.join(', ')}`)
86
+ }
87
+
88
+ //data
89
+ let data = await fetchWithRetry(url, optFetch)
90
+
91
+ //check
92
+ if (get(data, 'stat') !== 'OK') {
93
+ throw new Error(`TWSE T86 API returned: ${get(data, 'stat')}`)
94
+ }
95
+
96
+ //fields, rawData
97
+ let fields = get(data, 'fields')
98
+ let rawData = get(data, 'data')
99
+ if (!Array.isArray(rawData)) {
100
+ throw new Error('TWSE T86: data not found in response.')
101
+ }
102
+ if (!Array.isArray(fields)) {
103
+ throw new Error('TWSE T86: fields not found in response.')
104
+ }
105
+
106
+ //parsedData, 以API回傳之fields為key
107
+ let parsedData = rawData.map((row) => {
108
+ let obj = {}
109
+ fields.forEach((field, index) => {
110
+ let value = row[index]
111
+ if (typeof value === 'string') {
112
+ value = value.trim()
113
+ }
114
+ obj[field] = value
115
+ })
116
+ return obj
117
+ })
118
+
119
+ //過濾指定個股
120
+ if (targetCodes.length > 0) {
121
+ let codeField = fields.find((f) => String(f).includes('證券代號'))
122
+ if (!codeField) {
123
+ throw new Error('無法篩選個股:API 回應中找不到證券代號欄位')
124
+ }
125
+ parsedData = parsedData.filter((item) => targetCodes.includes(item[codeField]))
126
+ }
127
+
128
+ if (showLog) {
129
+ console.log(`Fetched ${parsedData.length} records.`)
130
+ }
131
+
132
+ return { source: 'twse', date: dateStr, data: parsedData }
133
+ }
134
+
135
+
136
+ export default fetchTwseT86