@watsonserve/stock-base 0.1.6 → 0.1.7
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/baidu.js +137 -0
- package/eastmoney.js +95 -0
- package/helper.js +19 -0
- package/hkex.js +191 -0
- package/index.js +103 -61
- package/log.js +5 -1
- package/package.json +2 -2
- package/sgx.js +111 -0
- package/types/baidu.d.ts +20 -0
- package/types/eastmoney.d.ts +14 -0
- package/types/helper.d.ts +4 -1
- package/types/hkex.d.ts +47 -0
- package/types/index.d.ts +15 -5
- package/types/sgx.d.ts +9 -0
- package/types/stock.d.ts +6 -0
- package/types/{us-holiday.d.ts → usa-holiday.d.ts} +2 -1
- package/types/web-request.d.ts +6 -8
- package/{us-holiday.js → usa-holiday.js} +4 -1
- package/web-request.js +36 -38
- package/yahoo-finance-loader.js +13 -27
- package/dividend-loader.js +0 -167
- package/history-loader.js +0 -103
- package/price-loader.js +0 -129
- package/types/dividend-loader.d.ts +0 -19
- package/types/history-loader.d.ts +0 -11
- package/types/price-loader.d.ts +0 -14
package/baidu.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { EnCurrency, EnMarket } from './stock.js';
|
|
2
|
+
import { getMarketCloseTime } from './close-time.js';
|
|
3
|
+
import WebRequest from './web-request.js';
|
|
4
|
+
import { sleep } from './helper.js';
|
|
5
|
+
export class BaiduLoader extends WebRequest {
|
|
6
|
+
static __instance;
|
|
7
|
+
async __fetch(url, method = 'GET', params) {
|
|
8
|
+
if (new URL(this.page.url()).origin !== 'https://finance.baidu.com')
|
|
9
|
+
throw new Error('Not on finance.baidu.com');
|
|
10
|
+
let token = '';
|
|
11
|
+
try {
|
|
12
|
+
await this.page.waitForFunction(() => !!window['paris_2108']?.getAcsToken, undefined, { timeout: 15000 });
|
|
13
|
+
token = await this.page.evaluate(() => new Promise(r => window['paris_2108'].getAcsToken(r)));
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new Error(`failed to get Acs-Token: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
return super.fetch(url, method, params, { 'Acs-Token': token });
|
|
19
|
+
}
|
|
20
|
+
// 匯率
|
|
21
|
+
async __loadFx(ncs) {
|
|
22
|
+
try {
|
|
23
|
+
const data = new Map();
|
|
24
|
+
for (const nc of ncs) {
|
|
25
|
+
const body = await this.__fetch('https://finance.pae.baidu.com/vapi/v1/sug', 'GET', { wd: `USD${nc}`, skip_login: 1, finClientType: 'pc' });
|
|
26
|
+
const fx = body.Result.list[0];
|
|
27
|
+
data.set(nc, +fx.price);
|
|
28
|
+
}
|
|
29
|
+
return Object.fromEntries(data.entries());
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new Error(`failed to load fx ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 标普500成份股
|
|
36
|
+
async __sp500(pn = 0, rn = 100) {
|
|
37
|
+
const params = {
|
|
38
|
+
financeType: 'index',
|
|
39
|
+
market: 'us',
|
|
40
|
+
code: 'SPX',
|
|
41
|
+
sortKey: 'marketValue',
|
|
42
|
+
sortType: 'desc',
|
|
43
|
+
style: 'tablelist',
|
|
44
|
+
finClientType: 'pc',
|
|
45
|
+
pn, rn,
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const resp = await this.__fetch('https://finance.pae.baidu.com/sapi/v1/constituents', 'GET', params);
|
|
49
|
+
const stocklist = resp.Result.list.body;
|
|
50
|
+
return stocklist.map(item => {
|
|
51
|
+
const { code: nc, name: n, rawData } = item;
|
|
52
|
+
const { lastPx: c, volume: v, marketValue: mv } = rawData;
|
|
53
|
+
return { nc, n, c, v, mv };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new Error(`failed to load s&p ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
static async factory() {
|
|
61
|
+
const baidu = new BaiduLoader();
|
|
62
|
+
await baidu._mount();
|
|
63
|
+
return baidu;
|
|
64
|
+
}
|
|
65
|
+
static async getInstance() {
|
|
66
|
+
if (BaiduLoader.__instance)
|
|
67
|
+
return BaiduLoader.__instance;
|
|
68
|
+
BaiduLoader.__instance = BaiduLoader.factory().catch(err => {
|
|
69
|
+
BaiduLoader.__instance = null;
|
|
70
|
+
throw err;
|
|
71
|
+
});
|
|
72
|
+
return BaiduLoader.__instance;
|
|
73
|
+
}
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
}
|
|
77
|
+
async _mount() {
|
|
78
|
+
await super._mount('https://finance.baidu.com/index/us-SPX?mainTab=成分股');
|
|
79
|
+
await sleep(1000);
|
|
80
|
+
}
|
|
81
|
+
async destroy() {
|
|
82
|
+
BaiduLoader.__instance = null;
|
|
83
|
+
await super.destroy();
|
|
84
|
+
}
|
|
85
|
+
async loadFxHistory() {
|
|
86
|
+
const fxs = await Promise.all([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY].map(code => this.__fetch('https://finance.pae.baidu.com/vapi/v1/getquotation', 'GET', { group: 'huilv_kline', ktype: 'day', code: `USD${code}`, count: 204, finClientType: 'pc' })));
|
|
87
|
+
const [hk, sg, cn] = fxs.map(body => body.Result.newMarketData.marketData.split(';'));
|
|
88
|
+
const base = new Map(hk.map(line => {
|
|
89
|
+
const [_0, date, _1, c] = line.split(',');
|
|
90
|
+
const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
|
|
91
|
+
return [stamp, { date, HKD: +c }];
|
|
92
|
+
}));
|
|
93
|
+
sg.forEach(line => {
|
|
94
|
+
const [_0, date, _1, c] = line.split(',');
|
|
95
|
+
const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
|
|
96
|
+
const fx = base.get(stamp) || { date };
|
|
97
|
+
fx.SGD = +c;
|
|
98
|
+
base.set(stamp, fx);
|
|
99
|
+
});
|
|
100
|
+
cn.map(line => {
|
|
101
|
+
const [_0, date, _1, c] = line.split(',');
|
|
102
|
+
const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
|
|
103
|
+
const fx = base.get(stamp) || { date };
|
|
104
|
+
fx.CNY = +c;
|
|
105
|
+
base.set(stamp, fx);
|
|
106
|
+
});
|
|
107
|
+
const result = [];
|
|
108
|
+
const remain = [];
|
|
109
|
+
[...base.entries()].forEach(([stamp, fx]) => {
|
|
110
|
+
const _fx = { stamp, ...fx };
|
|
111
|
+
if (!fx.HKD || !fx.SGD || !fx.CNY) {
|
|
112
|
+
remain.push(_fx);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
result.push(_fx);
|
|
116
|
+
});
|
|
117
|
+
return { result, remain };
|
|
118
|
+
}
|
|
119
|
+
async loadFx() {
|
|
120
|
+
const fxs = await this.__loadFx([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY]);
|
|
121
|
+
const stamp = getMarketCloseTime(EnMarket.FX);
|
|
122
|
+
return { ...fxs, stamp };
|
|
123
|
+
}
|
|
124
|
+
async loadPrices() {
|
|
125
|
+
let pn = 0, rn = 100;
|
|
126
|
+
let sts = [];
|
|
127
|
+
while (true) {
|
|
128
|
+
const _sts = await this.__sp500(pn, rn);
|
|
129
|
+
const _len = _sts.length;
|
|
130
|
+
sts = sts.concat(_sts);
|
|
131
|
+
if (_len < rn)
|
|
132
|
+
break;
|
|
133
|
+
pn += _len;
|
|
134
|
+
}
|
|
135
|
+
return sts;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/eastmoney.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { formatFullNc, sleep, ttm } from './helper.js';
|
|
2
|
+
import { EnMarket } from './stock.js';
|
|
3
|
+
import WebRequest from './web-request.js';
|
|
4
|
+
import { getMarketCloseTime } from './close-time.js';
|
|
5
|
+
export class EastmoneyLoader extends WebRequest {
|
|
6
|
+
static __instance;
|
|
7
|
+
static async factory() {
|
|
8
|
+
const eastmoney = new EastmoneyLoader();
|
|
9
|
+
await eastmoney._mount();
|
|
10
|
+
return eastmoney;
|
|
11
|
+
}
|
|
12
|
+
static async getInstance() {
|
|
13
|
+
if (EastmoneyLoader.__instance)
|
|
14
|
+
return EastmoneyLoader.__instance;
|
|
15
|
+
EastmoneyLoader.__instance = EastmoneyLoader.factory().catch(err => {
|
|
16
|
+
EastmoneyLoader.__instance = null;
|
|
17
|
+
throw err;
|
|
18
|
+
});
|
|
19
|
+
return EastmoneyLoader.__instance;
|
|
20
|
+
}
|
|
21
|
+
async destroy() {
|
|
22
|
+
EastmoneyLoader.__instance = null;
|
|
23
|
+
await super.destroy();
|
|
24
|
+
}
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
}
|
|
28
|
+
async loadDividend(nc) {
|
|
29
|
+
const params = {
|
|
30
|
+
reportName: 'RPT_USF10_INFO_DIVIDEND',
|
|
31
|
+
columns: 'SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,SECURITY_INNER_CODE,NOTICE_DATE,ASSIGN_TYPE,PLAN_EXPLAIN,EQUITY_RECORD_DATE,BONUS_PAY_DATE,EX_DIVIDEND_DATE,ASSIGN_PERIOD',
|
|
32
|
+
// filter: `(SECUCODE="${nc}.N")`,
|
|
33
|
+
filter: `(SECURITY_CODE="${nc.replace('.', '_')}")`,
|
|
34
|
+
pageNumber: 1,
|
|
35
|
+
pageSize: 200,
|
|
36
|
+
sortTypes: -1,
|
|
37
|
+
sortColumns: 'EX_DIVIDEND_DATE',
|
|
38
|
+
source: 'SECURITIES',
|
|
39
|
+
client: 'PC',
|
|
40
|
+
v: Math.random().toString().substring(2).padStart(17, '0')
|
|
41
|
+
};
|
|
42
|
+
const strResp = await super.fetch('https://datacenter.eastmoney.com/securities/api/data/v1/get', params);
|
|
43
|
+
const data = (JSON.parse(strResp).result?.data || []);
|
|
44
|
+
const dividend = [];
|
|
45
|
+
for (const item of data) {
|
|
46
|
+
const { ASSIGN_TYPE, PLAN_EXPLAIN, NOTICE_DATE, EX_DIVIDEND_DATE, BONUS_PAY_DATE } = item;
|
|
47
|
+
if ('Cash' === ASSIGN_TYPE || 'Special cash' === ASSIGN_TYPE) {
|
|
48
|
+
const amount = +PLAN_EXPLAIN.match(/(\d*\.?\d+)美元/)[1]; // 每1股派0.82美元股息
|
|
49
|
+
if (!amount)
|
|
50
|
+
console.warn(item);
|
|
51
|
+
dividend.push({
|
|
52
|
+
currency: 'USD',
|
|
53
|
+
amount,
|
|
54
|
+
annc: new Date(NOTICE_DATE),
|
|
55
|
+
ex: new Date(EX_DIVIDEND_DATE),
|
|
56
|
+
paid: new Date(BONUS_PAY_DATE),
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return formatFullNc(nc, 'US', ttm('ex', dividend), []);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @warning This method requires the Eastmoney page to be loaded and may not work if the page structure changes.
|
|
65
|
+
*/
|
|
66
|
+
async loadHistory(nc) {
|
|
67
|
+
await super.goto(`https://quote.eastmoney.com/us/${nc}.html`);
|
|
68
|
+
await sleep(500);
|
|
69
|
+
const secid = await super.read('quotecode');
|
|
70
|
+
const params = {
|
|
71
|
+
fields1: 'f1,f2,f3,f4,f5,f6',
|
|
72
|
+
fields2: 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
|
|
73
|
+
klt: 101, // 60 hour, 101 day, 102 week, 103 month
|
|
74
|
+
fqt: 0,
|
|
75
|
+
secid,
|
|
76
|
+
beg: 0,
|
|
77
|
+
end: 20500000,
|
|
78
|
+
// ut: 'bd1d9ddb04089700cf9c27f6f7426281',
|
|
79
|
+
// lmt: 1000000,
|
|
80
|
+
};
|
|
81
|
+
if (WebRequest.isDebug)
|
|
82
|
+
await sleep(5000);
|
|
83
|
+
const resp = await super.fetch('https://push2his.eastmoney.com/api/qt/stock/kline/get', params);
|
|
84
|
+
// const resp = JSON.parse(await fs.readFile(path.join(import.meta.dirname, 'hsbc.json'), 'utf-8'));
|
|
85
|
+
const { klines } = resp.data;
|
|
86
|
+
return klines.map(line => {
|
|
87
|
+
const [date, open, close, hight, low, vol] = line.split(',');
|
|
88
|
+
return {
|
|
89
|
+
market: EnMarket.USA,
|
|
90
|
+
timestamp: getMarketCloseTime(EnMarket.USA, new Date(`${date}T15:00:00Z`).getTime()),
|
|
91
|
+
st: { nc, n: '', o: +open, c: +close, h: +hight, l: +low, v: +vol }
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
package/helper.js
CHANGED
|
@@ -33,3 +33,22 @@ export function ttm(by, list) {
|
|
|
33
33
|
return sinceTime < Date.UTC(val.getUTCFullYear(), val.getUTCMonth());
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
export function toDivPoint(fullNc, div) {
|
|
37
|
+
const { annc, ex, paid, ...another } = div;
|
|
38
|
+
return {
|
|
39
|
+
...another,
|
|
40
|
+
nc: fullNc,
|
|
41
|
+
annc: ~~(annc.getTime() / 1000),
|
|
42
|
+
ex: ~~(ex.getTime() / 1000),
|
|
43
|
+
paid: ~~(paid.getTime() / 1000)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function parseUTCDate(str) {
|
|
47
|
+
return ~~(Date.UTC(+str.slice(0, 4), +str.slice(4, 6) - 1, +str.slice(6, 8)) / 1000);
|
|
48
|
+
}
|
|
49
|
+
export function formatFullNc(nc, market, div, bonus) {
|
|
50
|
+
const fullNc = `${nc}.${market}`;
|
|
51
|
+
const dps = div.map(item => toDivPoint(fullNc, item));
|
|
52
|
+
const bps = bonus.map(item => toDivPoint(fullNc, item));
|
|
53
|
+
return { fullNc, nc, dps, bps };
|
|
54
|
+
}
|
package/hkex.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import WebRequest from './web-request.js';
|
|
2
|
+
import { formatFullNc, parseUTCDate, ttm } from './helper.js';
|
|
3
|
+
import { EnMarket } from './stock.js';
|
|
4
|
+
import { A_DAY_MS, A_DAY_S } from './close-time.js';
|
|
5
|
+
export var EnInt;
|
|
6
|
+
(function (EnInt) {
|
|
7
|
+
EnInt[EnInt["day_1"] = 0] = "day_1";
|
|
8
|
+
EnInt[EnInt["mo_1"] = 2] = "mo_1";
|
|
9
|
+
EnInt[EnInt["mo_3"] = 3] = "mo_3";
|
|
10
|
+
EnInt[EnInt["mo_6"] = 4] = "mo_6";
|
|
11
|
+
EnInt[EnInt["yr_1"] = 5] = "yr_1";
|
|
12
|
+
EnInt[EnInt["yrs_2"] = 6] = "yrs_2";
|
|
13
|
+
EnInt[EnInt["yrs_5"] = 7] = "yrs_5";
|
|
14
|
+
EnInt[EnInt["yrs_10"] = 8] = "yrs_10";
|
|
15
|
+
EnInt[EnInt["ytd"] = 9] = "ytd";
|
|
16
|
+
})(EnInt || (EnInt = {}));
|
|
17
|
+
export var EnSpan;
|
|
18
|
+
(function (EnSpan) {
|
|
19
|
+
EnSpan[EnSpan["min"] = 0] = "min";
|
|
20
|
+
EnSpan[EnSpan["day"] = 6] = "day";
|
|
21
|
+
EnSpan[EnSpan["week"] = 7] = "week";
|
|
22
|
+
EnSpan[EnSpan["month"] = 8] = "month";
|
|
23
|
+
EnSpan[EnSpan["quarter"] = 9] = "quarter";
|
|
24
|
+
})(EnSpan || (EnSpan = {}));
|
|
25
|
+
const rxDate = /\d+/g;
|
|
26
|
+
function dateCNtoDate(str) {
|
|
27
|
+
const ymd = str.match(rxDate)?.slice(0, 3).map(i => +i);
|
|
28
|
+
if (!ymd)
|
|
29
|
+
return null;
|
|
30
|
+
if ('number' === typeof ymd?.[1]) {
|
|
31
|
+
ymd[1] -= 1;
|
|
32
|
+
}
|
|
33
|
+
return new Date(Date.UTC.apply(null, ymd));
|
|
34
|
+
}
|
|
35
|
+
const HKDict = Object.entries({
|
|
36
|
+
'The first day of January': 'New Year\'s Day',
|
|
37
|
+
'Lunar New Year’s Day': 'Chinese New Year',
|
|
38
|
+
'Hong Kong Special Administrative Region': 'HK SAR',
|
|
39
|
+
'香港公眾假期 -': '',
|
|
40
|
+
'Hong Kong Public Holidays -': '',
|
|
41
|
+
'The day following': ''
|
|
42
|
+
});
|
|
43
|
+
function simpleTitle(title = '') {
|
|
44
|
+
return HKDict.reduce((str, [k, v]) => str.replace(k, v), title).trim();
|
|
45
|
+
}
|
|
46
|
+
export class HKEXLoader extends WebRequest {
|
|
47
|
+
static HKEX_TOKEN = { token: '', t: 0 };
|
|
48
|
+
static tokenPromise = null;
|
|
49
|
+
static __instance;
|
|
50
|
+
async __fetchHkexToken() {
|
|
51
|
+
const doc = await super.fetch('https://www.hkex.com.hk/Market-Data/Securities-Prices/Equities', { sc_lang: 'zh-HK' });
|
|
52
|
+
const token = doc.split('//return "Base64-AES-Encrypted-Token";')[1].split('";')[0].split('"')[1];
|
|
53
|
+
const decodedToken = decodeURIComponent(token);
|
|
54
|
+
HKEXLoader.HKEX_TOKEN = { token: decodedToken, t: Date.now() };
|
|
55
|
+
return decodedToken;
|
|
56
|
+
}
|
|
57
|
+
async __loadHkexToken() {
|
|
58
|
+
const expiry = Date.now() - A_DAY_MS;
|
|
59
|
+
if (HKEXLoader.HKEX_TOKEN?.t > expiry)
|
|
60
|
+
return HKEXLoader.HKEX_TOKEN.token;
|
|
61
|
+
if (HKEXLoader.tokenPromise)
|
|
62
|
+
return HKEXLoader.tokenPromise;
|
|
63
|
+
HKEXLoader.tokenPromise = this.__fetchHkexToken().catch(err => {
|
|
64
|
+
HKEXLoader.tokenPromise = null;
|
|
65
|
+
throw err;
|
|
66
|
+
});
|
|
67
|
+
return HKEXLoader.tokenPromise;
|
|
68
|
+
}
|
|
69
|
+
static async factory() {
|
|
70
|
+
const hkex = new HKEXLoader();
|
|
71
|
+
await hkex._mount();
|
|
72
|
+
return hkex;
|
|
73
|
+
}
|
|
74
|
+
static async getInstance() {
|
|
75
|
+
if (HKEXLoader.__instance)
|
|
76
|
+
return HKEXLoader.__instance;
|
|
77
|
+
HKEXLoader.__instance = HKEXLoader.factory().catch(err => {
|
|
78
|
+
HKEXLoader.__instance = null;
|
|
79
|
+
throw err;
|
|
80
|
+
});
|
|
81
|
+
return HKEXLoader.__instance;
|
|
82
|
+
}
|
|
83
|
+
constructor() {
|
|
84
|
+
super();
|
|
85
|
+
}
|
|
86
|
+
async destroy() {
|
|
87
|
+
HKEXLoader.__instance = null;
|
|
88
|
+
await super.destroy();
|
|
89
|
+
}
|
|
90
|
+
async loadDividend(nc) {
|
|
91
|
+
const token = await this.__loadHkexToken();
|
|
92
|
+
const qid = Date.now();
|
|
93
|
+
const callback = `jsonp_${qid}`;
|
|
94
|
+
const params = { sym: +nc, token, lang: 'chi', recperpage: 5, page: 1, qid, callback };
|
|
95
|
+
const resp = await super.fetch('https://www1.hkex.com.hk/hkexwidget/data/getequityentitlement', params);
|
|
96
|
+
const strJson = resp.substring(callback.length + 1, resp.length - 1);
|
|
97
|
+
const { entitlementlist } = JSON.parse(strJson).data;
|
|
98
|
+
const list = [];
|
|
99
|
+
for (const item of entitlementlist) {
|
|
100
|
+
const { detail, announcement_date, ex_date, payment_date, announcement_URL } = item;
|
|
101
|
+
const annc = dateCNtoDate(announcement_date);
|
|
102
|
+
const ex = dateCNtoDate(ex_date);
|
|
103
|
+
const paid = dateCNtoDate(payment_date);
|
|
104
|
+
const divid = Array.from(detail.match(/(USD|HKD|GBP) (\d+\.\d+)/g) || [])
|
|
105
|
+
.reduce((pre, item) => {
|
|
106
|
+
const [currency, amount] = item.split(' ');
|
|
107
|
+
pre[currency] = { currency, amount: +amount };
|
|
108
|
+
return pre;
|
|
109
|
+
}, {});
|
|
110
|
+
list.push({ ...(divid['HKD'] || divid['USD'] || divid['GBP']), annc, ex, paid, announcement_URL });
|
|
111
|
+
}
|
|
112
|
+
return formatFullNc(nc, 'HK', ttm('ex', list), []);
|
|
113
|
+
}
|
|
114
|
+
// 香港交易所主板股票
|
|
115
|
+
async loadPrices() {
|
|
116
|
+
try {
|
|
117
|
+
const token = await this.__loadHkexToken();
|
|
118
|
+
const qid = Date.now();
|
|
119
|
+
const callback = `jsonp_${qid}`;
|
|
120
|
+
const params = {
|
|
121
|
+
lang: 'chi',
|
|
122
|
+
token,
|
|
123
|
+
sort: 5,
|
|
124
|
+
order: 0,
|
|
125
|
+
all: 1,
|
|
126
|
+
subcat: 1,
|
|
127
|
+
market: 'MAIN',
|
|
128
|
+
qid,
|
|
129
|
+
callback,
|
|
130
|
+
};
|
|
131
|
+
const resp = await super.fetch('https://www1.hkex.com.hk/hkexwidget/data/getequityfilter', params);
|
|
132
|
+
const strJson = resp.substring(callback.length + 1, resp.length - 1);
|
|
133
|
+
const body = JSON.parse(strJson);
|
|
134
|
+
const { stocklist } = body.data;
|
|
135
|
+
return stocklist.map(item => {
|
|
136
|
+
const { sym, nm: n, ls } = item;
|
|
137
|
+
return { nc: sym.padStart(5, '0'), n, c: +ls };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
throw new Error(`failed to load hkex ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async loadHolidays() {
|
|
145
|
+
const resp = await fetch('https://www.hkex.com.hk/News/HKEX-Calendar/Subscribe-Calendar?sc_lang=en');
|
|
146
|
+
const text = (await resp.text()).replace(/\r\n/g, '\n').replace(/\n\n/g, '\n'); // handle line folding
|
|
147
|
+
return text.split('\nEND:VEVENT\nBEGIN:VEVENT\n').reduce((pre, blk) => {
|
|
148
|
+
const ev = new Map(blk.split('\n').map(line => line.split(':', 2)));
|
|
149
|
+
if (!['香港市場休市', 'Hong Kong Market is closed'].includes(ev.get('DESCRIPTION') || ''))
|
|
150
|
+
return pre;
|
|
151
|
+
const start = parseUTCDate(ev.get('DTSTART;VALUE=DATE') || '');
|
|
152
|
+
let end = parseUTCDate(ev.get('DTEND;VALUE=DATE') || '');
|
|
153
|
+
if (start === end) {
|
|
154
|
+
end += A_DAY_S;
|
|
155
|
+
}
|
|
156
|
+
const lastOne = pre[pre.length - 1];
|
|
157
|
+
if (lastOne && start === lastOne.end) {
|
|
158
|
+
lastOne.end = end;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
pre.push({ market: 'HK', title: simpleTitle(ev.get('SUMMARY')), start, end });
|
|
162
|
+
}
|
|
163
|
+
return pre;
|
|
164
|
+
}, []);
|
|
165
|
+
}
|
|
166
|
+
// HSI
|
|
167
|
+
async loadHistory(nc, int = EnInt.yr_1) {
|
|
168
|
+
const token = await this.__loadHkexToken();
|
|
169
|
+
const qid = Date.now();
|
|
170
|
+
const callback = `jsonp_${qid}`;
|
|
171
|
+
const params = {
|
|
172
|
+
token, qid, callback,
|
|
173
|
+
hchart: 1,
|
|
174
|
+
span: EnSpan.day,
|
|
175
|
+
int,
|
|
176
|
+
ric: '.' === nc[0] ? nc : `${nc.padStart(5, '0').substring(1)}.HK`,
|
|
177
|
+
};
|
|
178
|
+
const resp = await super.fetch('https://www1.hkex.com.hk/hkexwidget/data/getchartdata2', params);
|
|
179
|
+
const strJson = resp.substring(callback.length + 1, resp.length - 1);
|
|
180
|
+
const body = JSON.parse(strJson);
|
|
181
|
+
const datalist = body.data.datalist.slice(1, -1);
|
|
182
|
+
return datalist.map(item => {
|
|
183
|
+
const [timestamp, o, h, l, c, v] = item;
|
|
184
|
+
return {
|
|
185
|
+
market: EnMarket.HKEX,
|
|
186
|
+
timestamp: ~~(new Date(`${new Date(timestamp).toJSON().substring(0, 10)}T08:10:00Z`).getTime() / 1000),
|
|
187
|
+
st: { nc, n: '', o, h, l, c, v }
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
package/index.js
CHANGED
|
@@ -1,74 +1,116 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { BaiduLoader } from './baidu.js';
|
|
2
|
+
import { EastmoneyLoader } from './eastmoney.js';
|
|
3
|
+
import { EnMarket } from './stock.js';
|
|
4
|
+
import WebRequest from './web-request.js';
|
|
5
|
+
import { splitFullNc, ttm } from './helper.js';
|
|
6
|
+
import { HKEXLoader } from './hkex.js';
|
|
7
|
+
import { SGXLoader } from './sgx.js';
|
|
8
|
+
import { generateUSHolidays } from './usa-holiday.js';
|
|
4
9
|
export * from './close-time.js';
|
|
5
10
|
export * from './stock.js';
|
|
6
11
|
export * from './log.js';
|
|
7
12
|
export * from './helper.js';
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return HKDict.reduce((str, [k, v]) => str.replace(k, v), title).trim();
|
|
13
|
+
async function __load(Gen, fn) {
|
|
14
|
+
const ins = await Gen.getInstance();
|
|
15
|
+
let result = undefined;
|
|
16
|
+
let err = undefined;
|
|
17
|
+
try {
|
|
18
|
+
result = await fn(ins);
|
|
19
|
+
}
|
|
20
|
+
catch (_err) {
|
|
21
|
+
err = _err;
|
|
22
|
+
}
|
|
23
|
+
ins.destroy();
|
|
24
|
+
return err ? Promise.reject(err) : result;
|
|
21
25
|
}
|
|
22
|
-
export class StockLoader
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const text = (await resp.text()).replace(/\r\n/g, '\n').replace(/\n\n/g, '\n'); // handle line folding
|
|
26
|
-
return text.split('\nEND:VEVENT\nBEGIN:VEVENT\n').reduce((pre, blk) => {
|
|
27
|
-
const ev = new Map(blk.split('\n').map(line => line.split(':', 2)));
|
|
28
|
-
let start = parseUTCDate(ev.get('DTSTART;VALUE=DATE') || '');
|
|
29
|
-
let end = parseUTCDate(ev.get('DTEND;VALUE=DATE') || '');
|
|
30
|
-
if (start === end) {
|
|
31
|
-
end += A_DAY_S;
|
|
32
|
-
}
|
|
33
|
-
// recording to SG calendar, we will move it to next day(Monday, 1) if holiday is Sunday(0)
|
|
34
|
-
if (!new Date(start * 1000).getUTCDay()) {
|
|
35
|
-
end += A_DAY_S;
|
|
36
|
-
}
|
|
37
|
-
const firstOne = pre[0];
|
|
38
|
-
const title = ev.get('SUMMARY') || '';
|
|
39
|
-
(firstOne && end === firstOne.start) ? Object.assign(firstOne, { title, start }) : pre.unshift({ market: 'SG', title, start, end });
|
|
40
|
-
return pre;
|
|
41
|
-
}, []);
|
|
26
|
+
export class StockLoader {
|
|
27
|
+
get isDebug() {
|
|
28
|
+
return WebRequest.isDebug;
|
|
42
29
|
}
|
|
43
|
-
async
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}, []);
|
|
30
|
+
static async init(isDebug = false) {
|
|
31
|
+
return WebRequest.init(isDebug);
|
|
32
|
+
}
|
|
33
|
+
async loadFxHistory() {
|
|
34
|
+
return __load(BaiduLoader, ins => ins.loadFxHistory());
|
|
35
|
+
}
|
|
36
|
+
async loadFx() {
|
|
37
|
+
return __load(BaiduLoader, ins => ins.loadFx());
|
|
38
|
+
}
|
|
39
|
+
async loadPrices(market) {
|
|
40
|
+
switch (market) {
|
|
41
|
+
case EnMarket.SGX:
|
|
42
|
+
return new SGXLoader().loadPrices();
|
|
43
|
+
case EnMarket.HKEX:
|
|
44
|
+
return __load(HKEXLoader, ins => ins.loadPrices());
|
|
45
|
+
case EnMarket.USA:
|
|
46
|
+
return __load(BaiduLoader, ins => ins.loadPrices());
|
|
47
|
+
default:
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Market ${market} not supported`);
|
|
64
50
|
}
|
|
65
51
|
async loadHolidays() {
|
|
66
|
-
const yyyy = new Date().getUTCFullYear();
|
|
67
52
|
const [holidaysSG, holidaysHK, holidaysUS] = await Promise.all([
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
generateUSHolidays(
|
|
53
|
+
new SGXLoader().loadHolidays().catch(() => []),
|
|
54
|
+
__load(HKEXLoader, ins => ins.loadHolidays()),
|
|
55
|
+
generateUSHolidays()
|
|
71
56
|
]);
|
|
72
57
|
return holidaysSG.concat(holidaysHK).concat(holidaysUS);
|
|
73
58
|
}
|
|
59
|
+
async loadHistory(fullNc) {
|
|
60
|
+
const { nc, market } = splitFullNc(fullNc);
|
|
61
|
+
switch (market) {
|
|
62
|
+
case EnMarket.SGX:
|
|
63
|
+
return new SGXLoader().loadHistory(nc);
|
|
64
|
+
case EnMarket.HKEX:
|
|
65
|
+
return __load(HKEXLoader, ins => ins.loadHistory(nc));
|
|
66
|
+
case EnMarket.USA:
|
|
67
|
+
return __load(EastmoneyLoader, ins => ins.loadHistory(nc));
|
|
68
|
+
default:
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Market ${market} not supported`);
|
|
71
|
+
}
|
|
72
|
+
_splitByMarket(ncs) {
|
|
73
|
+
const foo = {
|
|
74
|
+
[EnMarket.SGX]: [],
|
|
75
|
+
[EnMarket.HKEX]: [],
|
|
76
|
+
[EnMarket.USA]: [],
|
|
77
|
+
};
|
|
78
|
+
for (const fullNc of ncs) {
|
|
79
|
+
const { nc, market } = splitFullNc(fullNc);
|
|
80
|
+
if (!market)
|
|
81
|
+
continue;
|
|
82
|
+
foo[market].push(nc);
|
|
83
|
+
}
|
|
84
|
+
return foo;
|
|
85
|
+
}
|
|
86
|
+
async loadDividends(ncs) {
|
|
87
|
+
const { [EnMarket.SGX]: SG, [EnMarket.HKEX]: HK, [EnMarket.USA]: US } = this._splitByMarket(ncs);
|
|
88
|
+
let sgDivs = Promise.resolve([]);
|
|
89
|
+
let hkDivs = Promise.resolve([]);
|
|
90
|
+
let usDivs = Promise.resolve([]);
|
|
91
|
+
if (SG.length) {
|
|
92
|
+
const sgxLoader = new SGXLoader();
|
|
93
|
+
sgDivs = Promise.all(SG.map(async (nc) => sgxLoader.loadDividend(nc)));
|
|
94
|
+
}
|
|
95
|
+
if (HK.length) {
|
|
96
|
+
hkDivs = __load(HKEXLoader, ins => Promise.all(HK.map(nc => ins.loadDividend(nc))));
|
|
97
|
+
}
|
|
98
|
+
if (US.length) {
|
|
99
|
+
usDivs = __load(EastmoneyLoader, ins => Promise.all(US.map(nc => ins.loadDividend(nc))));
|
|
100
|
+
}
|
|
101
|
+
const divs = (await Promise.all([sgDivs, hkDivs, usDivs])).flat();
|
|
102
|
+
const ign = [];
|
|
103
|
+
let points = [];
|
|
104
|
+
const divPool = {};
|
|
105
|
+
for (const { fullNc, nc, dps, bps } of divs) {
|
|
106
|
+
if (!dps.length && !bps.length) {
|
|
107
|
+
ign.push(fullNc);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
points = points.concat(dps, bps);
|
|
111
|
+
}
|
|
112
|
+
divPool[nc] = ttm('ex', dps);
|
|
113
|
+
}
|
|
114
|
+
return { ign, points, divPool };
|
|
115
|
+
}
|
|
74
116
|
}
|
package/log.js
CHANGED
|
@@ -24,7 +24,11 @@ export class Log {
|
|
|
24
24
|
log(lev, msg) {
|
|
25
25
|
if (lev > this.level)
|
|
26
26
|
return Promise.resolve();
|
|
27
|
-
|
|
27
|
+
const out = EnLogLevel[lev].toLowerCase();
|
|
28
|
+
msg = `[${new Date().toISOString()}] ${msg}\n`;
|
|
29
|
+
if (this.level & EnLogLevel.DEBUG)
|
|
30
|
+
console[out](msg);
|
|
31
|
+
return appendFile(`${this._logDir}/${out}.log`, msg);
|
|
28
32
|
}
|
|
29
33
|
debug(msg) {
|
|
30
34
|
return this.log(EnLogLevel.DEBUG, msg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@watsonserve/stock-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"author": "",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"peerDependencies": {
|
|
12
|
-
"playwright": "^1.
|
|
12
|
+
"playwright": "^1.65.0"
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|