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,99 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTaifex from '../src/fetchTaifex.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTaifex', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, interRequestDelayMs: 0, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期格式錯誤與日期不合法之訊息不同', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
await fetchTaifex('2026-08-07', optFast)
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
r.push(err.message)
|
|
27
|
+
})
|
|
28
|
+
await fetchTaifex('20260230', optFast)
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message)
|
|
31
|
+
})
|
|
32
|
+
let rr = [
|
|
33
|
+
'dateStr 須為 YYYYMMDD 字串,得到: 2026-08-07',
|
|
34
|
+
'dateStr 非合法日期: 20260230',
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Big5編碼之CSV可正確解碼與解析', async function() {
|
|
40
|
+
let t = await fetchTaifex('20260807', optFast)
|
|
41
|
+
let r = [t.date, t.errors, t.futures.tx.contractMonth]
|
|
42
|
+
let rr = ['20260807', [], '202608']
|
|
43
|
+
assert.strict.deepEqual(r, rr)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('台指期取近月合約, 排除到期月份含斜線之價差合約', async function() {
|
|
47
|
+
let t = await fetchTaifex('20260807', optFast)
|
|
48
|
+
let rr = {
|
|
49
|
+
contractMonth: '202608',
|
|
50
|
+
open: 23000,
|
|
51
|
+
high: 23200,
|
|
52
|
+
low: 22900,
|
|
53
|
+
close: 23150,
|
|
54
|
+
settlement: 23160,
|
|
55
|
+
volume: 80000,
|
|
56
|
+
afterHoursClose: 23250,
|
|
57
|
+
afterHoursSettlement: 23260,
|
|
58
|
+
afterHoursVolume: 20000,
|
|
59
|
+
}
|
|
60
|
+
assert.strict.deepEqual(t.futures.tx, rr)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('三大法人期貨未平倉依身份別轉為foreign、trust與dealers', async function() {
|
|
64
|
+
let t = await fetchTaifex('20260807', optFast)
|
|
65
|
+
let r = [
|
|
66
|
+
Object.keys(t.institutional).sort(),
|
|
67
|
+
t.institutional.foreign.netContracts,
|
|
68
|
+
t.institutional.trust.netContracts,
|
|
69
|
+
t.institutional.dealers.netContracts,
|
|
70
|
+
]
|
|
71
|
+
let rr = [['dealers', 'foreign', 'trust'], 20000, 1500, 1000]
|
|
72
|
+
assert.strict.deepEqual(r, rr)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('Put/Call Ratio解析為數值', async function() {
|
|
76
|
+
let t = await fetchTaifex('20260807', optFast)
|
|
77
|
+
let rr = {
|
|
78
|
+
putVolume: 100000,
|
|
79
|
+
callVolume: 90000,
|
|
80
|
+
ratio: 111.11,
|
|
81
|
+
putOpenInterest: 50000,
|
|
82
|
+
callOpenInterest: 60000,
|
|
83
|
+
openInterestRatio: 83.33,
|
|
84
|
+
}
|
|
85
|
+
assert.strict.deepEqual(t.pcRatio, rr)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('三支資料全失敗時reject並帶errors', async function() {
|
|
89
|
+
let e = null
|
|
90
|
+
await fetchTaifex('20260807', { ...optFast, baseUrl: svr.url('/no-data') })
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
e = err
|
|
93
|
+
})
|
|
94
|
+
let r = [e.message.startsWith('所有資料抓取失敗'), e.errors.length]
|
|
95
|
+
let rr = [true, 3]
|
|
96
|
+
assert.strict.deepEqual(r, rr)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTpex3insti from '../src/fetchTpex3insti.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTpex3insti', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期格式錯誤與日期不合法之訊息不同', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
await fetchTpex3insti('2026-08-07', [], optFast)
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
r.push(err.message)
|
|
27
|
+
})
|
|
28
|
+
await fetchTpex3insti('20260230', [], optFast)
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message)
|
|
31
|
+
})
|
|
32
|
+
let rr = [
|
|
33
|
+
'dateStr must be YYYYMMDD, got: 2026-08-07',
|
|
34
|
+
'dateStr 不是合法日期: 20260230',
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('以固定位置還原7組明細欄名, 不因裸欄名重複而塌成1組', async function() {
|
|
40
|
+
let t = await fetchTpex3insti('20260807', ['6488'], optFast)
|
|
41
|
+
let rr = {
|
|
42
|
+
'代號': '6488',
|
|
43
|
+
'名稱': '環球晶',
|
|
44
|
+
'外陸資買進股數(不含外資自營商)': '100',
|
|
45
|
+
'外陸資賣出股數(不含外資自營商)': '50',
|
|
46
|
+
'外陸資買賣超股數(不含外資自營商)': '50',
|
|
47
|
+
'外資自營商買進股數': '10',
|
|
48
|
+
'外資自營商賣出股數': '5',
|
|
49
|
+
'外資自營商買賣超股數': '5',
|
|
50
|
+
'外資及陸資買進股數': '110',
|
|
51
|
+
'外資及陸資賣出股數': '55',
|
|
52
|
+
'外資及陸資買賣超股數': '55',
|
|
53
|
+
'投信買進股數': '200',
|
|
54
|
+
'投信賣出股數': '100',
|
|
55
|
+
'投信買賣超股數': '100',
|
|
56
|
+
'自營商買進股數(自行買賣)': '30',
|
|
57
|
+
'自營商賣出股數(自行買賣)': '10',
|
|
58
|
+
'自營商買賣超股數(自行買賣)': '20',
|
|
59
|
+
'自營商買進股數(避險)': '40',
|
|
60
|
+
'自營商賣出股數(避險)': '20',
|
|
61
|
+
'自營商買賣超股數(避險)': '20',
|
|
62
|
+
'自營商買進股數': '70',
|
|
63
|
+
'自營商賣出股數': '30',
|
|
64
|
+
'自營商買賣超股數': '40',
|
|
65
|
+
'三大法人買賣超股數合計': '195',
|
|
66
|
+
}
|
|
67
|
+
assert.strict.deepEqual(t.data[0], rr)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('未指定個股時回傳全市場', async function() {
|
|
71
|
+
let t = await fetchTpex3insti('20260807', [], optFast)
|
|
72
|
+
let r = [t.source, t.date, t.data.length]
|
|
73
|
+
let rr = ['tpex', '20260807', 2]
|
|
74
|
+
assert.strict.deepEqual(r, rr)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('欄序已變時fail-loud, 避免把外資數字貼到投信頭上', async function() {
|
|
78
|
+
let r = null
|
|
79
|
+
await fetchTpex3insti('20260807', [], { ...optFast, baseUrl: svr.url('/bad-shape') })
|
|
80
|
+
.then(() => {
|
|
81
|
+
r = 'resolved'
|
|
82
|
+
})
|
|
83
|
+
.catch((err) => {
|
|
84
|
+
r = err.message
|
|
85
|
+
})
|
|
86
|
+
let rr = 'TPEX 3insti: 欄數異常,預期 24 得 6(疑似 API 改版,請重新校準 TPEX_FIELD_MAP)'
|
|
87
|
+
assert.strict.deepEqual(r, rr)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTpexMargin from '../src/fetchTpexMargin.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTpexMargin', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期格式錯誤與日期不合法之訊息不同', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
await fetchTpexMargin('2026-08-07', [], optFast)
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
r.push(err.message)
|
|
27
|
+
})
|
|
28
|
+
await fetchTpexMargin('20260230', [], optFast)
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message)
|
|
31
|
+
})
|
|
32
|
+
let rr = [
|
|
33
|
+
'日期參數無效:格式須為 YYYYMMDD (收到 "2026-08-07")',
|
|
34
|
+
'日期參數無效:不合法的日期 (20260230)',
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('依固定欄序解析融資融券並算出增減', async function() {
|
|
40
|
+
let t = await fetchTpexMargin('20260807', ['6488'], optFast)
|
|
41
|
+
let rr = {
|
|
42
|
+
code: '6488',
|
|
43
|
+
name: '環球晶',
|
|
44
|
+
marginBuy: 300,
|
|
45
|
+
marginSell: 200,
|
|
46
|
+
marginCashRepay: 50,
|
|
47
|
+
marginPrevBalance: 5000,
|
|
48
|
+
marginBalance: 5050,
|
|
49
|
+
marginChange: 50,
|
|
50
|
+
marginLimit: 400000,
|
|
51
|
+
shortSell: 100,
|
|
52
|
+
shortBuy: 80,
|
|
53
|
+
shortCashRepay: 20,
|
|
54
|
+
shortPrevBalance: 1000,
|
|
55
|
+
shortBalance: 1000,
|
|
56
|
+
shortChange: 0,
|
|
57
|
+
shortLimit: 400000,
|
|
58
|
+
offset: 30,
|
|
59
|
+
note: '',
|
|
60
|
+
}
|
|
61
|
+
assert.strict.deepEqual(t.data[0], rr)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('未指定個股時回傳全市場', async function() {
|
|
65
|
+
let t = await fetchTpexMargin('20260807', [], optFast)
|
|
66
|
+
let r = [t.source, t.date, t.count]
|
|
67
|
+
let rr = ['tpex_margin', '20260807', 2]
|
|
68
|
+
assert.strict.deepEqual(r, rr)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('欄位佈局與預期不符時fail-loud, 不產生錯誤資料', async function() {
|
|
72
|
+
let r = null
|
|
73
|
+
await fetchTpexMargin('20260807', [], { ...optFast, baseUrl: svr.url('/bad-shape') })
|
|
74
|
+
.then(() => {
|
|
75
|
+
r = 'resolved'
|
|
76
|
+
})
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
r = err.message.startsWith('TPEX 融資融券資料欄位佈局與預期不符')
|
|
79
|
+
})
|
|
80
|
+
let rr = true
|
|
81
|
+
assert.strict.deepEqual(r, rr)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('指定個股查無資料時reject', async function() {
|
|
85
|
+
let r = null
|
|
86
|
+
await fetchTpexMargin('20260807', ['2330'], optFast)
|
|
87
|
+
.catch((err) => {
|
|
88
|
+
r = err.message
|
|
89
|
+
})
|
|
90
|
+
let rr = '指定個股 2330 不在上櫃融資融券資料中(可能為上市股或代碼有誤)'
|
|
91
|
+
assert.strict.deepEqual(r, rr)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTpexStock from '../src/fetchTpexStock.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTpexStock', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期非合法YYYYMMDD時拋錯', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
for (let dateStr of ['', null, '2026-08-07', '20260230']) {
|
|
25
|
+
await fetchTpexStock(dateStr, [], optFast)
|
|
26
|
+
.then(() => {
|
|
27
|
+
r.push('resolved')
|
|
28
|
+
})
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message.startsWith('dateStr 須為合法之 YYYYMMDD 字串'))
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
let rr = [true, true, true, true]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('未指定個股時回傳全市場', async function() {
|
|
38
|
+
let t = await fetchTpexStock('20260807', undefined, optFast)
|
|
39
|
+
let r = [t.source, t.date, t.count, t.data[0][0]]
|
|
40
|
+
let rr = ['tpex', '20260807', 2, '6488']
|
|
41
|
+
assert.strict.deepEqual(r, rr)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('選表優先取標題含「行情」者而非第一張有資料表', async function() {
|
|
45
|
+
let t = await fetchTpexStock('20260807', [], optFast)
|
|
46
|
+
let r = [t.count, t.data[0].length >= 8]
|
|
47
|
+
let rr = [2, true]
|
|
48
|
+
assert.strict.deepEqual(r, rr)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('指定個股時過濾為該個股', async function() {
|
|
52
|
+
let t = await fetchTpexStock('20260807', ['6510'], optFast)
|
|
53
|
+
let r = [t.count, t.data[0][0], t.data[0][1]]
|
|
54
|
+
let rr = [1, '6510', '精測']
|
|
55
|
+
assert.strict.deepEqual(r, rr)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('指定個股查無資料時reject且訊息與整體無資料明確區分', async function() {
|
|
59
|
+
let r = null
|
|
60
|
+
await fetchTpexStock('20260807', ['2330'], optFast)
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
r = err.message
|
|
63
|
+
})
|
|
64
|
+
let rr = '指定個股 2330 在 20260807 之上櫃資料中查無資料(可能為上市股、代碼有誤、或當日無交易)'
|
|
65
|
+
assert.strict.deepEqual(r, rr)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTwDataHoliday from '../src/fetchTwDataHoliday.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTwDataHoliday', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { url: svr.url('/v1/holidaySchedule/holidaySchedule'), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('民國日期轉西元並依日期排序, 交易日標記與結算作業日被排除且同日去重', async function() {
|
|
23
|
+
let t = await fetchTwDataHoliday(undefined, optFast)
|
|
24
|
+
let rr = {
|
|
25
|
+
dataYear: '2026',
|
|
26
|
+
totalHolidays: 2,
|
|
27
|
+
holidays: [
|
|
28
|
+
{
|
|
29
|
+
date: '20260101',
|
|
30
|
+
rocDate: '1150101',
|
|
31
|
+
name: '中華民國開國紀念日',
|
|
32
|
+
weekday: '四',
|
|
33
|
+
description: '1月1日(星期四)為假日。',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
date: '20260216',
|
|
37
|
+
rocDate: '1150216',
|
|
38
|
+
name: '農曆春節',
|
|
39
|
+
weekday: '一',
|
|
40
|
+
description: '2月16日放假',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
assert.strict.deepEqual(t, rr)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('checkDate為休市日時回傳isHoliday為true與假日名稱', async function() {
|
|
48
|
+
let t = await fetchTwDataHoliday('20260101', optFast)
|
|
49
|
+
let r = [t.checkDate, t.isHoliday, t.holidayName]
|
|
50
|
+
let rr = ['20260101', true, '中華民國開國紀念日']
|
|
51
|
+
assert.strict.deepEqual(r, rr)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('checkDate非休市日時回傳isHoliday為false與null', async function() {
|
|
55
|
+
let t = await fetchTwDataHoliday('20260102', optFast)
|
|
56
|
+
let r = [t.checkDate, t.isHoliday, t.holidayName]
|
|
57
|
+
let rr = ['20260102', false, null]
|
|
58
|
+
assert.strict.deepEqual(r, rr)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('checkDate非合法YYYYMMDD時略過單日比對, 不誤標isHoliday', async function() {
|
|
62
|
+
let r = []
|
|
63
|
+
for (let checkDate of ['', null, undefined, '2026-01-01', 20260101]) {
|
|
64
|
+
let t = await fetchTwDataHoliday(checkDate, optFast)
|
|
65
|
+
r.push([t.checkDate, t.isHoliday, t.holidayName])
|
|
66
|
+
}
|
|
67
|
+
let rr = [
|
|
68
|
+
[undefined, undefined, undefined],
|
|
69
|
+
[undefined, undefined, undefined],
|
|
70
|
+
[undefined, undefined, undefined],
|
|
71
|
+
[undefined, undefined, undefined],
|
|
72
|
+
[undefined, undefined, undefined],
|
|
73
|
+
]
|
|
74
|
+
assert.strict.deepEqual(r, rr)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('API回傳非JSON時給出友善訊息而非裸SyntaxError', async function() {
|
|
78
|
+
let r = null
|
|
79
|
+
await fetchTwDataHoliday(undefined, { ...optFast, url: svr.url('/json/bad') })
|
|
80
|
+
.then(() => {
|
|
81
|
+
r = 'resolved'
|
|
82
|
+
})
|
|
83
|
+
.catch((err) => {
|
|
84
|
+
r = [err.message, err instanceof SyntaxError]
|
|
85
|
+
})
|
|
86
|
+
let rr = ['API 回傳非 JSON 格式(可能為維護頁面或網路中介),請稍後再試', false]
|
|
87
|
+
assert.strict.deepEqual(r, rr)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('API回傳非陣列時reject', async function() {
|
|
91
|
+
let r = null
|
|
92
|
+
await fetchTwDataHoliday(undefined, { ...optFast, url: svr.url('/json/ok') })
|
|
93
|
+
.catch((err) => {
|
|
94
|
+
r = err.message
|
|
95
|
+
})
|
|
96
|
+
let rr = 'API 回傳空陣列或格式異常'
|
|
97
|
+
assert.strict.deepEqual(r, rr)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTwseMargin from '../src/fetchTwseMargin.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTwseMargin', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期格式錯誤與日期不合法之訊息不同', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
await fetchTwseMargin('2026-08-07', [], optFast)
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
r.push(err.message)
|
|
27
|
+
})
|
|
28
|
+
await fetchTwseMargin('20260230', [], optFast)
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message)
|
|
31
|
+
})
|
|
32
|
+
let rr = [
|
|
33
|
+
'日期參數無效:格式須為 YYYYMMDD (收到 "2026-08-07")',
|
|
34
|
+
'日期參數無效:不合法的日期 (20260230)',
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('以標題「融資融券彙總」選表, 不誤取第一張有資料表', async function() {
|
|
40
|
+
let t = await fetchTwseMargin('20260807', [], optFast)
|
|
41
|
+
let r = [t.source, t.date, t.count, t.data[0].code, t.data[0].name]
|
|
42
|
+
let rr = ['twse_margin', '20260807', 2, '2330', '台積電']
|
|
43
|
+
assert.strict.deepEqual(r, rr)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('依固定欄序解析融資融券並算出增減', async function() {
|
|
47
|
+
let t = await fetchTwseMargin('20260807', ['2330'], optFast)
|
|
48
|
+
let rr = {
|
|
49
|
+
code: '2330',
|
|
50
|
+
name: '台積電',
|
|
51
|
+
marginBuy: 1000,
|
|
52
|
+
marginSell: 500,
|
|
53
|
+
marginCashRepay: 100,
|
|
54
|
+
marginPrevBalance: 10000,
|
|
55
|
+
marginBalance: 10400,
|
|
56
|
+
marginChange: 400,
|
|
57
|
+
marginLimit: 99999,
|
|
58
|
+
shortSell: 300,
|
|
59
|
+
shortBuy: 200,
|
|
60
|
+
shortCashRepay: 50,
|
|
61
|
+
shortPrevBalance: 2000,
|
|
62
|
+
shortBalance: 2250,
|
|
63
|
+
shortChange: 250,
|
|
64
|
+
shortLimit: 88888,
|
|
65
|
+
offset: 10,
|
|
66
|
+
note: '',
|
|
67
|
+
}
|
|
68
|
+
assert.strict.deepEqual(t.data[0], rr)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('指定個股查無資料時reject', async function() {
|
|
72
|
+
let r = null
|
|
73
|
+
await fetchTwseMargin('20260807', ['6488'], optFast)
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
r = err.message
|
|
76
|
+
})
|
|
77
|
+
let rr = '指定個股 6488 不在上市融資融券資料中(可能為上櫃股或代碼有誤)'
|
|
78
|
+
assert.strict.deepEqual(r, rr)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTwseStock from '../src/fetchTwseStock.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTwseStock', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期非合法YYYYMMDD時拋錯', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
for (let dateStr of ['', null, undefined, '2026-08-07', '20260230', 20260807]) {
|
|
25
|
+
await fetchTwseStock(dateStr, '2330', optFast)
|
|
26
|
+
.then(() => {
|
|
27
|
+
r.push('resolved')
|
|
28
|
+
})
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message.startsWith('dateStr 須為合法之 YYYYMMDD 字串'))
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
let rr = [true, true, true, true, true, true]
|
|
34
|
+
assert.strict.deepEqual(r, rr)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('指定個股走STOCK_DAY並過濾為指定日單筆', async function() {
|
|
38
|
+
let t = await fetchTwseStock('20260807', '2330', optFast)
|
|
39
|
+
let r = [t.stat, t.data.length, t.data[0][0], t.data[0][6], t.fields.length]
|
|
40
|
+
let rr = ['OK', 1, '115/08/07', '1,115.00', 9]
|
|
41
|
+
assert.strict.deepEqual(r, rr)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('未指定個股或指定all時走MI_INDEX取全市場', async function() {
|
|
45
|
+
let t1 = await fetchTwseStock('20260807', undefined, optFast)
|
|
46
|
+
let t2 = await fetchTwseStock('20260807', 'all', optFast)
|
|
47
|
+
let t3 = await fetchTwseStock('20260807', 'ALL', optFast)
|
|
48
|
+
let r = [t1.tables[0].data.length, t2.tables[0].data.length, t3.tables[0].data.length]
|
|
49
|
+
let rr = [2, 2, 2]
|
|
50
|
+
assert.strict.deepEqual(r, rr)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('指定日無交易資料時reject並與整體無資料區分', async function() {
|
|
54
|
+
let r = null
|
|
55
|
+
await fetchTwseStock('20260808', '2330', optFast)
|
|
56
|
+
.then(() => {
|
|
57
|
+
r = 'resolved'
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
r = err.message
|
|
61
|
+
})
|
|
62
|
+
let rr = 'TWSE 個股 2330 於 20260808 無交易資料(可能為假日或停盤)'
|
|
63
|
+
assert.strict.deepEqual(r, rr)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('API回傳stat非OK時reject且不重試', async function() {
|
|
67
|
+
let c0 = svr.getCount('/exchangeReport/STOCK_DAY')
|
|
68
|
+
let r = null
|
|
69
|
+
await fetchTwseStock('20090101', '2330', { ...optFast, maxRetries: 3 })
|
|
70
|
+
.then(() => {
|
|
71
|
+
r = 'resolved'
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
r = [err.message.startsWith('TWSE API returned:'), svr.getCount('/exchangeReport/STOCK_DAY') - c0]
|
|
75
|
+
})
|
|
76
|
+
let rr = [true, 1]
|
|
77
|
+
assert.strict.deepEqual(r, rr)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fetchTwseT86 from '../src/fetchTwseT86.mjs'
|
|
3
|
+
import serverForTest from './tools/serverForTest.mjs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('fetchTwseT86', function() {
|
|
7
|
+
|
|
8
|
+
let svr = null
|
|
9
|
+
let optFast = null
|
|
10
|
+
|
|
11
|
+
before(async function() {
|
|
12
|
+
svr = await serverForTest()
|
|
13
|
+
optFast = { baseUrl: svr.url(''), baseDelayMs: 10, maxDelayMs: 10, showLog: false }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
after(async function() {
|
|
17
|
+
if (svr) {
|
|
18
|
+
await svr.close()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('日期格式錯誤與日期不合法之訊息不同', async function() {
|
|
23
|
+
let r = []
|
|
24
|
+
await fetchTwseT86('2026-08-07', [], optFast)
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
r.push(err.message)
|
|
27
|
+
})
|
|
28
|
+
await fetchTwseT86('20260230', [], optFast)
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
r.push(err.message)
|
|
31
|
+
})
|
|
32
|
+
let rr = [
|
|
33
|
+
'dateStr must be YYYYMMDD, got: 2026-08-07',
|
|
34
|
+
'dateStr 不是合法日期: 20260230',
|
|
35
|
+
]
|
|
36
|
+
assert.strict.deepEqual(r, rr)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('以API回傳之fields為key轉為物件陣列並去除欄位前後空白', async function() {
|
|
40
|
+
let t = await fetchTwseT86('20260807', [], optFast)
|
|
41
|
+
let rr = {
|
|
42
|
+
source: 'twse',
|
|
43
|
+
date: '20260807',
|
|
44
|
+
data: [
|
|
45
|
+
{
|
|
46
|
+
'證券代號': '2330',
|
|
47
|
+
'證券名稱': '台積電',
|
|
48
|
+
'外陸資買進股數(不含外資自營商)': '1,000,000',
|
|
49
|
+
'三大法人買賣超股數': '500,000',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
'證券代號': '2317',
|
|
53
|
+
'證券名稱': '鴻海',
|
|
54
|
+
'外陸資買進股數(不含外資自營商)': '800,000',
|
|
55
|
+
'三大法人買賣超股數': '-200,000',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
assert.strict.deepEqual(t, rr)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('指定個股時以證券代號欄過濾', async function() {
|
|
63
|
+
let t = await fetchTwseT86('20260807', ['2317'], optFast)
|
|
64
|
+
let r = [t.data.length, t.data[0]['證券代號']]
|
|
65
|
+
let rr = [1, '2317']
|
|
66
|
+
assert.strict.deepEqual(r, rr)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('指定個股皆不存在時回傳空陣列', async function() {
|
|
70
|
+
let t = await fetchTwseT86('20260807', ['9999'], optFast)
|
|
71
|
+
let r = t.data
|
|
72
|
+
let rr = []
|
|
73
|
+
assert.strict.deepEqual(r, rr)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
})
|