@watsonserve/stock-loader 0.0.1 → 0.0.2-alpha.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/build.mjs +28 -0
- package/package.json +9 -7
- package/src/close-time.ts +123 -0
- package/src/dividend-loader.ts +196 -0
- package/src/helper.ts +36 -0
- package/src/history-loader.ts +71 -0
- package/src/index.ts +73 -0
- package/src/price-loader.ts +125 -0
- package/src/web-request.ts +131 -0
- package/tsconfig.json +22 -0
- package/close-time.js +0 -110
- package/dividend-loader.js +0 -148
- package/helper.js +0 -23
- package/history-loader.js +0 -65
- package/index.js +0 -8
- package/price-loader.js +0 -122
- package/types/close-time.d.ts +0 -11
- package/types/dividend-loader.d.ts +0 -14
- package/types/helper.d.ts +0 -15
- package/types/history-loader.d.ts +0 -17
- package/types/index.d.ts +0 -6
- package/types/price-loader.d.ts +0 -16
- package/types/web-request.d.ts +0 -21
- package/web-request.js +0 -115
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
|
|
2
|
+
import { A_DAY_MS } from './close-time.js';
|
|
3
|
+
|
|
4
|
+
export default class WebRequest {
|
|
5
|
+
private static HKEX_TOKEN = { token: '', t: 0 };
|
|
6
|
+
private static browser?: Browser;
|
|
7
|
+
private static context?: BrowserContext;
|
|
8
|
+
private static pageCnt = 0;
|
|
9
|
+
private static __promise?: Promise<void>;
|
|
10
|
+
protected static isDebug: boolean = false;
|
|
11
|
+
protected page?: Page;
|
|
12
|
+
|
|
13
|
+
private static async __getInstance(debug = false) {
|
|
14
|
+
if (WebRequest.context) return;
|
|
15
|
+
// 1. 启动 Chromium,禁用 web security(解除 CORS)
|
|
16
|
+
WebRequest.browser = await chromium.launch({
|
|
17
|
+
headless: !debug,
|
|
18
|
+
channel: 'msedge',
|
|
19
|
+
args: [
|
|
20
|
+
'--disable-web-security',
|
|
21
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
22
|
+
'--no-sandbox',
|
|
23
|
+
'--disable-setuid-sandbox'
|
|
24
|
+
]
|
|
25
|
+
});
|
|
26
|
+
WebRequest.context = await WebRequest.browser.newContext({});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get isDebug() {
|
|
30
|
+
return WebRequest.isDebug;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static async init(isDebug = false) {
|
|
34
|
+
WebRequest.isDebug = isDebug;
|
|
35
|
+
if (!WebRequest.__promise)
|
|
36
|
+
WebRequest.__promise = WebRequest.__getInstance(isDebug);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor() {}
|
|
40
|
+
|
|
41
|
+
async mount() {
|
|
42
|
+
await WebRequest.__promise;
|
|
43
|
+
|
|
44
|
+
this.page = await WebRequest.context!.newPage();
|
|
45
|
+
WebRequest.pageCnt++;
|
|
46
|
+
await this.page.goto('about:blank');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static async destroy() {
|
|
50
|
+
WebRequest.pageCnt--;
|
|
51
|
+
if (0 < WebRequest.pageCnt) return;
|
|
52
|
+
|
|
53
|
+
await WebRequest.context?.close();
|
|
54
|
+
await WebRequest.browser?.close();
|
|
55
|
+
WebRequest.context = undefined;
|
|
56
|
+
WebRequest.browser = undefined;
|
|
57
|
+
WebRequest.pageCnt = 0;
|
|
58
|
+
WebRequest.__promise = undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async destroy() {
|
|
62
|
+
if (!this.page) return;
|
|
63
|
+
|
|
64
|
+
await this.page.close();
|
|
65
|
+
this.page = undefined;
|
|
66
|
+
return WebRequest.destroy();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected fetch(url: string, method: string = 'GET', params?: Record<string, any>, headers?: Record<string, any>) {
|
|
70
|
+
method = method.toUpperCase();
|
|
71
|
+
let data = '';
|
|
72
|
+
if (params) {
|
|
73
|
+
if (['HEAD', 'GET'].includes(method)) {
|
|
74
|
+
const uri = new URL(url);
|
|
75
|
+
const urlParams = Object.fromEntries(uri.searchParams);
|
|
76
|
+
|
|
77
|
+
uri.search = new URLSearchParams(Object.assign(urlParams, params)).toString();
|
|
78
|
+
url = uri.toString();
|
|
79
|
+
} else {
|
|
80
|
+
const ct = (headers?.['Content-Type'] || '').split(';')[0];
|
|
81
|
+
switch (ct) {
|
|
82
|
+
case 'application/json':
|
|
83
|
+
data = JSON.stringify(params);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
data = new URLSearchParams(params).toString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.page!.evaluate(async (conf: any) => {
|
|
93
|
+
const resp = await fetch(conf.url, {
|
|
94
|
+
method: conf.method,
|
|
95
|
+
credentials: 'include', // 如果需要 Cookie
|
|
96
|
+
headers: conf.headers,
|
|
97
|
+
body: conf.data || undefined,
|
|
98
|
+
});
|
|
99
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
100
|
+
const ct = resp.headers.get('Content-Type')?.split(';')[0] || '';
|
|
101
|
+
if ('application/json' === ct) return resp.json();
|
|
102
|
+
if (ct.startsWith('text/')) return resp.text();
|
|
103
|
+
return resp.arrayBuffer();
|
|
104
|
+
}, { method, url, headers, data });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected get(url: string, params?: Record<string, any>, headers?: Record<string, any>) {
|
|
108
|
+
return this.fetch(url, 'GET', params, headers);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected goto(pageUrl: string) {
|
|
112
|
+
return this.page!.goto(pageUrl);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protected read<T>(key: string) : Promise<T> {
|
|
116
|
+
return this.page!.evaluate(async (conf: any) => {
|
|
117
|
+
return (window as any)[conf.key];
|
|
118
|
+
}, { key });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected async _loadHkexToken() {
|
|
122
|
+
const expiry = Date.now() - A_DAY_MS;
|
|
123
|
+
if (WebRequest.HKEX_TOKEN?.t > expiry) return WebRequest.HKEX_TOKEN.token;
|
|
124
|
+
|
|
125
|
+
const doc = await this.get('https://www.hkex.com.hk/Market-Data/Securities-Prices/Equities?sc_lang=zh-HK');
|
|
126
|
+
const token = doc.split('//return "Base64-AES-Encrypted-Token";')[1].split('";')[0].split('"')[1];
|
|
127
|
+
const decodedToken = decodeURIComponent(token);
|
|
128
|
+
WebRequest.HKEX_TOKEN = { token: decodedToken, t: Date.now() };
|
|
129
|
+
return decodedToken;
|
|
130
|
+
}
|
|
131
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext", // 👈 关键:编译成 CommonJS 模块
|
|
4
|
+
"module": "NodeNext", // 👈 关键:Node.js 默认用 CommonJS
|
|
5
|
+
"lib": ["es2022", "dom"], // 包含 includes + 浏览器 API
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "NodeNext", // 👈 关键:用 Node.js 模块解析
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"sourceMap": false,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src",
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationDir": "./dist/types",
|
|
18
|
+
"types": ["node", "playwright"] // 显式包含类型
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/*.ts"], // 包含根目录 .ts 文件
|
|
21
|
+
"exclude": ["node_modules", "build.mjs"]
|
|
22
|
+
}
|
package/close-time.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
export const A_MIN_MS = 60000;
|
|
3
|
-
export const A_HOUR_MS = 3600000;
|
|
4
|
-
export const A_DAY_MS = 86400000;
|
|
5
|
-
// 夏令时:3月最后一个周日凌晨1AM开始夏令时,10月最后一个周日凌晨2AM结束夏令时
|
|
6
|
-
function isDST_ByUK(timestamp) {
|
|
7
|
-
const date = new Date(timestamp);
|
|
8
|
-
const year = date.getUTCFullYear();
|
|
9
|
-
// 英国夏令时:3月最后一个周日开始,10月最后一个周日结束
|
|
10
|
-
const march = new Date(Date.UTC(year, 2, 31));
|
|
11
|
-
const october = new Date(Date.UTC(year, 9, 31));
|
|
12
|
-
// 夏令时开始和结束的日期
|
|
13
|
-
const dstStart = new Date(Date.UTC(year, 2, 31 - march.getUTCDay(), 1)); // 3月最后一个周日凌晨1点
|
|
14
|
-
const dstEnd = new Date(Date.UTC(year, 9, 31 - october.getUTCDay(), 1)); // 10月最后一个周日凌晨2点 = UTC时间凌晨1点
|
|
15
|
-
return timestamp >= dstStart.getTime() && timestamp < dstEnd.getTime();
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 判断给定时间戳(毫秒)是否处于美国东部夏令时(EDT)
|
|
19
|
-
* 夏令时:3月第二个周日 02:00 开始(时钟拨快至 03:00),11月第一个周日 02:00 结束(时钟拨回至 01:00)
|
|
20
|
-
*/
|
|
21
|
-
function isDST_ByEST(timestamp) {
|
|
22
|
-
const date = new Date(timestamp);
|
|
23
|
-
const year = date.getUTCFullYear();
|
|
24
|
-
// 计算3月1日是星期几(UTC)
|
|
25
|
-
const march = new Date(Date.UTC(year, 2, 1)); // UTC 时间:3月1日 00:00
|
|
26
|
-
const november = new Date(Date.UTC(year, 10, 1));
|
|
27
|
-
const marchDay = (7 - march.getUTCDay()) % 7 + 8; // 第二个周日(1~7是第一个周日,8~14是第二个)
|
|
28
|
-
const novemberDay = (7 - november.getUTCDay()) % 7 + 1; // 第一个周日
|
|
29
|
-
const dstStart = Date.UTC(year, 2, marchDay, 7); // UTC 时间 7点
|
|
30
|
-
const dstEnd = Date.UTC(year, 10, novemberDay, 6); // UTC 时间 6点
|
|
31
|
-
// 处于夏令时:在开始之后,结束之前
|
|
32
|
-
return timestamp >= dstStart && timestamp < dstEnd;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* 获取市场 正式开盘时区偏移,单位分钟
|
|
36
|
-
* @param dst 是否夏令时
|
|
37
|
-
* @returns 分钟数
|
|
38
|
-
*/
|
|
39
|
-
function getOpenTimeByUTC(market, time) {
|
|
40
|
-
switch (market) {
|
|
41
|
-
case EnMarket.JPX:
|
|
42
|
-
return 0; // 9:00
|
|
43
|
-
case EnMarket.CHN:
|
|
44
|
-
return 90; // 9:30
|
|
45
|
-
case EnMarket.HKEX:
|
|
46
|
-
return 90; // 9:30
|
|
47
|
-
case EnMarket.SGX:
|
|
48
|
-
return 60; // 9:00
|
|
49
|
-
case EnMarket.LSE:
|
|
50
|
-
return isDST_ByUK(time) ? 420 : 480; // 7:00 or 8:00
|
|
51
|
-
case EnMarket.USA:
|
|
52
|
-
return isDST_ByEST(time) ? 810 : 870; // 13:30 or 14:30
|
|
53
|
-
default:
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`Unsupported market: ${market}`);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 获取市场时区偏移,单位分钟
|
|
59
|
-
* @param dst 是否夏令时
|
|
60
|
-
* @returns 分钟数
|
|
61
|
-
*/
|
|
62
|
-
function getCloseTimeByUTC(market, time) {
|
|
63
|
-
switch (market) {
|
|
64
|
-
case EnMarket.JPX:
|
|
65
|
-
return 390; // 6:30
|
|
66
|
-
case EnMarket.CHN:
|
|
67
|
-
return 420; // 7:00
|
|
68
|
-
case EnMarket.HKEX:
|
|
69
|
-
return 490; // 8:10
|
|
70
|
-
case EnMarket.SGX:
|
|
71
|
-
return 556; // 9:16
|
|
72
|
-
case EnMarket.LSE:
|
|
73
|
-
return isDST_ByUK(time) ? 930 : 990; // 15:30 or 16:30
|
|
74
|
-
case EnMarket.USA:
|
|
75
|
-
return isDST_ByEST(time) ? 1200 : 1260; // 20:00 or 21:00
|
|
76
|
-
case EnMarket.FX:
|
|
77
|
-
return isDST_ByEST(time) ? 1260 : 1320; // 21:00 or 22:00
|
|
78
|
-
default:
|
|
79
|
-
}
|
|
80
|
-
throw new Error(`Unsupported market: ${market}`);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 获取市场收盘时间戳,单位秒
|
|
84
|
-
* @param market 市场
|
|
85
|
-
* @param time 可选时间戳(毫秒),默认为当前时间,用于确定日期和夏令时状态
|
|
86
|
-
* @returns 市场收盘秒级时间戳
|
|
87
|
-
*/
|
|
88
|
-
export function getMarketCloseTime(market, time = Date.now()) {
|
|
89
|
-
const d = new Date(time); // 确保 time 是有效的时间戳
|
|
90
|
-
switch (d.getUTCDay()) {
|
|
91
|
-
case 6:
|
|
92
|
-
time -= 86400000;
|
|
93
|
-
break;
|
|
94
|
-
case 0:
|
|
95
|
-
time -= 172800000;
|
|
96
|
-
break;
|
|
97
|
-
default:
|
|
98
|
-
}
|
|
99
|
-
const dayStart = (~~(time / A_DAY_MS)) * A_DAY_MS;
|
|
100
|
-
const closeTimestamp = dayStart + getCloseTimeByUTC(market, time) * A_MIN_MS;
|
|
101
|
-
let openTimestamp = 0;
|
|
102
|
-
if (EnMarket.FX !== market) {
|
|
103
|
-
openTimestamp = dayStart + getOpenTimeByUTC(market, time) * A_MIN_MS;
|
|
104
|
-
}
|
|
105
|
-
// after market open time, return close time of the day
|
|
106
|
-
if (openTimestamp < time)
|
|
107
|
-
return ~~(closeTimestamp / 1000);
|
|
108
|
-
// return close time of the previous day
|
|
109
|
-
return getMarketCloseTime(market, closeTimestamp - A_DAY_MS);
|
|
110
|
-
}
|
package/dividend-loader.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { log } from '@watsonserve/stock-base';
|
|
2
|
-
import PriceLoader from './price-loader.js';
|
|
3
|
-
import { ttm } from './helper.js';
|
|
4
|
-
function toDivPoint(fullNc, div) {
|
|
5
|
-
const { annc, ex, paid, ...another } = div;
|
|
6
|
-
return {
|
|
7
|
-
...another,
|
|
8
|
-
nc: fullNc,
|
|
9
|
-
annc: ~~(annc.getTime() / 1000),
|
|
10
|
-
ex: ~~(ex.getTime() / 1000),
|
|
11
|
-
paid: ~~(paid.getTime() / 1000)
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
const rxDate = /\d+/g;
|
|
15
|
-
export class DividendLoader extends PriceLoader {
|
|
16
|
-
async __laodIbmCode(nc) {
|
|
17
|
-
const resp = await super.get(`https://api.sgx.com/marketmetadata/v2?stock-code=${nc}`);
|
|
18
|
-
return resp.data?.[0]?.ibmCode || '';
|
|
19
|
-
}
|
|
20
|
-
async __loadHKDividend(nc) {
|
|
21
|
-
const token = await super._loadHkexToken();
|
|
22
|
-
const qid = Date.now();
|
|
23
|
-
const callback = `jsonp_${qid}`;
|
|
24
|
-
const params = { sym: +nc, token, lang: 'chi', recperpage: 5, page: 1, qid, callback };
|
|
25
|
-
const resp = await super.get('https://www1.hkex.com.hk/hkexwidget/data/getequityentitlement', params);
|
|
26
|
-
const strJson = resp.substring(callback.length + 1, resp.length - 1);
|
|
27
|
-
const { entitlementlist } = JSON.parse(strJson).data;
|
|
28
|
-
const list = [];
|
|
29
|
-
for (const item of entitlementlist) {
|
|
30
|
-
const { detail, announcement_date, ex_date, payment_date, announcement_URL } = item;
|
|
31
|
-
const announcement_ymd = announcement_date.match(rxDate).slice(0, 3);
|
|
32
|
-
const ex_ymd = ex_date.match(rxDate)?.slice(0, 3);
|
|
33
|
-
const payment_ymd = payment_date.match(rxDate)?.slice(0, 3);
|
|
34
|
-
const divid = Array.from(detail.match(/(USD|HKD|GBP) (\d+\.\d+)/g) || [])
|
|
35
|
-
.reduce((pre, item) => {
|
|
36
|
-
const [currency, amount] = item.split(' ');
|
|
37
|
-
pre[currency] = { currency, amount: +amount };
|
|
38
|
-
return pre;
|
|
39
|
-
}, {});
|
|
40
|
-
list.push({
|
|
41
|
-
...(divid['HKD'] || divid['USD'] || divid['GBP']),
|
|
42
|
-
annc: new Date(Date.UTC.apply(null, announcement_ymd)),
|
|
43
|
-
ex: ex_ymd && new Date(Date.UTC.apply(null, ex_ymd)) || null,
|
|
44
|
-
paid: payment_ymd && new Date(Date.UTC.apply(null, payment_ymd)) || null,
|
|
45
|
-
announcement_URL
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
return [ttm('ex', list), []];
|
|
49
|
-
}
|
|
50
|
-
async __loadSGDividend(nc) {
|
|
51
|
-
const ibmcode = await this.__laodIbmCode(nc);
|
|
52
|
-
const params = {
|
|
53
|
-
pagesize: 15,
|
|
54
|
-
pagestart: 0,
|
|
55
|
-
ibmcode,
|
|
56
|
-
params: 'id,anncType,dateAnnc,exDate,name,particulars,recDate,datePaid',
|
|
57
|
-
order: 'desc',
|
|
58
|
-
orderBy: 'dateAnnc'
|
|
59
|
-
};
|
|
60
|
-
const { data } = await super.get('https://api.sgx.com/corporateactions/v1.0', params);
|
|
61
|
-
const bonus = [];
|
|
62
|
-
const dividend = [];
|
|
63
|
-
for (const item of data) {
|
|
64
|
-
const { anncType, particulars, dateAnnc, exDate, recDate, datePaid } = item;
|
|
65
|
-
const annc = new Date(dateAnnc);
|
|
66
|
-
const ex = new Date(exDate);
|
|
67
|
-
const rec = new Date(recDate); // 登记日
|
|
68
|
-
const paid = new Date(datePaid);
|
|
69
|
-
if ('DIVIDEND' === anncType) {
|
|
70
|
-
const amount = +particulars.match(/SGD (\d+\.\d+)/)?.[1] || 0;
|
|
71
|
-
const lstIdx = dividend.length - 1;
|
|
72
|
-
if (dividend[lstIdx]?.ex.getTime() === ex.getTime() && amount) {
|
|
73
|
-
dividend[lstIdx].amount += amount;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
dividend.push({ currency: 'SGD', amount, annc, ex, rec, paid });
|
|
77
|
-
}
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if ('BONUS' === anncType) { // particulars: 'Ratio: 10:1'
|
|
81
|
-
const ratio = particulars.match(/Ratio: (\d+:\d+)/);
|
|
82
|
-
const [foo, bar] = ratio[1].split(':');
|
|
83
|
-
bonus.push({ currency: 'SGD', ratio: 1 + (+bar) / (+foo), annc, ex, rec, paid });
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return [ttm('ex', dividend), bonus];
|
|
88
|
-
}
|
|
89
|
-
async __loadUSDividend(nc) {
|
|
90
|
-
const params = {
|
|
91
|
-
reportName: 'RPT_USF10_INFO_DIVIDEND',
|
|
92
|
-
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',
|
|
93
|
-
// filter: `(SECUCODE="${nc}.N")`,
|
|
94
|
-
filter: `(SECURITY_CODE="${nc.replace('.', '_')}")`,
|
|
95
|
-
pageNumber: 1,
|
|
96
|
-
pageSize: 200,
|
|
97
|
-
sortTypes: -1,
|
|
98
|
-
sortColumns: 'EX_DIVIDEND_DATE',
|
|
99
|
-
source: 'SECURITIES',
|
|
100
|
-
client: 'PC',
|
|
101
|
-
v: Math.random().toString().substring(2).padStart(17, '0')
|
|
102
|
-
};
|
|
103
|
-
const strResp = await super.get('https://datacenter.eastmoney.com/securities/api/data/v1/get', params);
|
|
104
|
-
const data = (JSON.parse(strResp).result?.data || []);
|
|
105
|
-
const dividend = [];
|
|
106
|
-
for (const item of data) {
|
|
107
|
-
const { ASSIGN_TYPE, PLAN_EXPLAIN, NOTICE_DATE, EX_DIVIDEND_DATE, BONUS_PAY_DATE } = item;
|
|
108
|
-
if ('Cash' === ASSIGN_TYPE || 'Special cash' === ASSIGN_TYPE) {
|
|
109
|
-
const amount = +PLAN_EXPLAIN.match(/(\d*\.?\d+)美元/)[1]; // 每1股派0.82美元股息
|
|
110
|
-
if (!amount)
|
|
111
|
-
console.warn(item);
|
|
112
|
-
dividend.push({
|
|
113
|
-
currency: 'USD',
|
|
114
|
-
amount,
|
|
115
|
-
annc: new Date(NOTICE_DATE),
|
|
116
|
-
ex: new Date(EX_DIVIDEND_DATE),
|
|
117
|
-
paid: new Date(BONUS_PAY_DATE),
|
|
118
|
-
});
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return [ttm('ex', dividend), []];
|
|
123
|
-
}
|
|
124
|
-
async loadDividend(fullNc) {
|
|
125
|
-
const nc = fullNc.substring(0, fullNc.length - 3);
|
|
126
|
-
const market = fullNc.substring(fullNc.length - 2);
|
|
127
|
-
let data;
|
|
128
|
-
log.info(`start loading ${fullNc} dividend...`);
|
|
129
|
-
switch (market) {
|
|
130
|
-
case 'SG':
|
|
131
|
-
data = await this.__loadSGDividend(nc);
|
|
132
|
-
break;
|
|
133
|
-
case 'HK':
|
|
134
|
-
data = await this.__loadHKDividend(nc);
|
|
135
|
-
break;
|
|
136
|
-
case 'US':
|
|
137
|
-
data = await this.__loadUSDividend(nc);
|
|
138
|
-
break;
|
|
139
|
-
default:
|
|
140
|
-
throw new Error(`Unsupported market: ${market}`);
|
|
141
|
-
}
|
|
142
|
-
const [div, bonus] = data;
|
|
143
|
-
const dps = div.map(item => toDivPoint(fullNc, item));
|
|
144
|
-
const bps = bonus.map(item => toDivPoint(fullNc, item));
|
|
145
|
-
log.info(`finished loading ${fullNc} dividend`);
|
|
146
|
-
return { fullNc, nc, dps, bps };
|
|
147
|
-
}
|
|
148
|
-
}
|
package/helper.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
export function sleep(t = 0) {
|
|
3
|
-
return new Promise(resolve => setTimeout(resolve, t));
|
|
4
|
-
}
|
|
5
|
-
export function splitFullNc(fullNc) {
|
|
6
|
-
const nc = fullNc.substring(0, fullNc.length - 3);
|
|
7
|
-
const market = {
|
|
8
|
-
US: EnMarket.USA,
|
|
9
|
-
SG: EnMarket.SGX,
|
|
10
|
-
HK: EnMarket.HKEX
|
|
11
|
-
}[fullNc.substring(fullNc.length - 2)];
|
|
12
|
-
return { nc, market };
|
|
13
|
-
}
|
|
14
|
-
export function ttm(by, list) {
|
|
15
|
-
if (!Array.isArray(list) || !list.length)
|
|
16
|
-
return [];
|
|
17
|
-
const latest = new Date(list[0][by]);
|
|
18
|
-
const sinceTime = Date.UTC(latest.getUTCFullYear() - 1, latest.getUTCMonth());
|
|
19
|
-
return list.filter(item => {
|
|
20
|
-
const val = new Date(item[by]);
|
|
21
|
-
return sinceTime < Date.UTC(val.getUTCFullYear(), val.getUTCMonth());
|
|
22
|
-
});
|
|
23
|
-
}
|
package/history-loader.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
import { getMarketCloseTime } from './close-time.js';
|
|
3
|
-
import WebRequest from './web-request.js';
|
|
4
|
-
import { sleep, splitFullNc } from './helper.js';
|
|
5
|
-
// 历史数据
|
|
6
|
-
export default class HistoryLoader extends WebRequest {
|
|
7
|
-
async __getHKOrUSAHistory(market, nc) {
|
|
8
|
-
let cntry = EnMarket.USA === market ? 'us' : 'hk';
|
|
9
|
-
await this.goto(`https://quote.eastmoney.com/${cntry}/${nc}.html`);
|
|
10
|
-
let secid = `116.${nc}`;
|
|
11
|
-
if (EnMarket.USA === market) {
|
|
12
|
-
await sleep(500);
|
|
13
|
-
// secid = await this.get_em_code(nc);
|
|
14
|
-
secid = await this.read('quotecode');
|
|
15
|
-
}
|
|
16
|
-
const params = {
|
|
17
|
-
fields1: 'f1,f2,f3,f4,f5,f6',
|
|
18
|
-
fields2: 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
|
|
19
|
-
klt: 101, // 60 hour, 101 day, 102 week, 103 month
|
|
20
|
-
fqt: 0,
|
|
21
|
-
secid,
|
|
22
|
-
beg: 0,
|
|
23
|
-
end: 20500000,
|
|
24
|
-
// ut: 'bd1d9ddb04089700cf9c27f6f7426281',
|
|
25
|
-
// lmt: 1000000,
|
|
26
|
-
};
|
|
27
|
-
if (this.isDebug) {
|
|
28
|
-
await sleep(5000);
|
|
29
|
-
}
|
|
30
|
-
const resp = await super.get('https://push2his.eastmoney.com/api/qt/stock/kline/get', params);
|
|
31
|
-
// const resp = JSON.parse(await fs.readFile(path.join(import.meta.dirname, 'hsbc.json'), 'utf-8'));
|
|
32
|
-
const { klines } = resp.data;
|
|
33
|
-
return klines.map(line => {
|
|
34
|
-
const [date, open, close, hight, low, vol] = line.split(',');
|
|
35
|
-
return {
|
|
36
|
-
market, nc, n: '', o: +open, c: +close, h: +hight, l: +low, v: +vol,
|
|
37
|
-
stamp: getMarketCloseTime(market, new Date(`${date}T15:00:00Z`).getTime())
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
async __getSGXHistory(nc) {
|
|
42
|
-
const rsp = await super.get(`https://api.sgx.com/securities/v1.1//charts/historic/stocks/code/${nc}/5y`, { params: 'trading_time,vl,lt' });
|
|
43
|
-
const { historic } = rsp.data;
|
|
44
|
-
return historic.map(st => {
|
|
45
|
-
const { trading_time, vl: v, lt: c } = st;
|
|
46
|
-
const fullYear = trading_time.substring(0, 4);
|
|
47
|
-
const month = trading_time.substring(4, 6);
|
|
48
|
-
const date = trading_time.substring(6, 8);
|
|
49
|
-
const stamp = ~~(new Date(`${fullYear}-${month}-${date}T09:16:00Z`).getTime() / 1000);
|
|
50
|
-
return { market: EnMarket.SGX, nc, n: '', o: 0, c, h: 0, l: 0, v, stamp };
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
loadHistory(fullNc) {
|
|
54
|
-
const { nc, market } = splitFullNc(fullNc);
|
|
55
|
-
switch (market) {
|
|
56
|
-
case EnMarket.SGX:
|
|
57
|
-
return this.__getSGXHistory(nc);
|
|
58
|
-
case EnMarket.HKEX:
|
|
59
|
-
case EnMarket.USA:
|
|
60
|
-
return this.__getHKOrUSAHistory(market, nc);
|
|
61
|
-
default:
|
|
62
|
-
}
|
|
63
|
-
throw new Error(`Unsupported market: ${market}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
package/index.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import WebRequest from './web-request.js';
|
|
2
|
-
import { DividendLoader } from './dividend-loader.js';
|
|
3
|
-
export { ttm, sleep, splitFullNc } from './helper.js';
|
|
4
|
-
export class StockLoader extends DividendLoader {
|
|
5
|
-
static async getInstance(debug = false) {
|
|
6
|
-
return WebRequest.getInstance(debug, new StockLoader());
|
|
7
|
-
}
|
|
8
|
-
}
|
package/price-loader.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { EnCurrency, EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
import { getMarketCloseTime } from './close-time.js';
|
|
3
|
-
import HistoryLoader from './history-loader.js';
|
|
4
|
-
export default class PriceLoader extends HistoryLoader {
|
|
5
|
-
// 匯率
|
|
6
|
-
async __loadFx(ncs) {
|
|
7
|
-
try {
|
|
8
|
-
const data = new Map();
|
|
9
|
-
for (const nc of ncs) {
|
|
10
|
-
const body = await this.get('https://finance.pae.baidu.com/selfselect/sug', { wd: `USD${nc}`, skip_login: 1, finClientType: 'pc' });
|
|
11
|
-
const fx = body.Result.stock[0];
|
|
12
|
-
data.set(nc, +fx.price);
|
|
13
|
-
}
|
|
14
|
-
return Object.fromEntries(data.entries());
|
|
15
|
-
}
|
|
16
|
-
catch (err) {
|
|
17
|
-
throw new Error(`failed to load fx ${err.message}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
async loadFx() {
|
|
21
|
-
const fxs = await this.__loadFx([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY]);
|
|
22
|
-
const stamp = getMarketCloseTime(EnMarket.FX);
|
|
23
|
-
return { ...fxs, stamp };
|
|
24
|
-
}
|
|
25
|
-
// 新加坡交易所全量股票
|
|
26
|
-
async sgx() {
|
|
27
|
-
try {
|
|
28
|
-
const body = await this.get('https://api.sgx.com/securities/v1.1/stocks', { params: 'nc,lt,ptd,n,o,h,l,vl,trading_time' });
|
|
29
|
-
const stockList = body.data.prices;
|
|
30
|
-
return stockList.map(({ trading_time: t, nc, lt: c, n, o, h, l, vl: v }) => ({ t, nc, n, o, c, h, l, v }));
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
throw new Error(`failed to load sgx ${err.message}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// 香港交易所主板股票
|
|
37
|
-
async hkex() {
|
|
38
|
-
try {
|
|
39
|
-
const token = await this._loadHkexToken();
|
|
40
|
-
const qid = Date.now();
|
|
41
|
-
const callback = `jsonp_${qid}`;
|
|
42
|
-
const params = {
|
|
43
|
-
lang: 'chi',
|
|
44
|
-
token,
|
|
45
|
-
sort: 5,
|
|
46
|
-
order: 0,
|
|
47
|
-
all: 1,
|
|
48
|
-
subcat: 1,
|
|
49
|
-
market: 'MAIN',
|
|
50
|
-
qid,
|
|
51
|
-
callback,
|
|
52
|
-
};
|
|
53
|
-
const resp = await this.get('https://www1.hkex.com.hk/hkexwidget/data/getequityfilter', params);
|
|
54
|
-
const strJson = resp.substring(callback.length + 1, resp.length - 1);
|
|
55
|
-
const body = JSON.parse(strJson);
|
|
56
|
-
const { stocklist } = body.data;
|
|
57
|
-
return stocklist.map(item => {
|
|
58
|
-
const { sym, nm: n, ls } = item;
|
|
59
|
-
return { nc: sym.padStart(5, '0'), n, c: +ls };
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
throw new Error(`failed to load hkex ${err.message}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// 标普500成份股
|
|
67
|
-
async __sp500(pn = 0, rn = 100) {
|
|
68
|
-
const params = {
|
|
69
|
-
financeType: 'index',
|
|
70
|
-
market: 'us',
|
|
71
|
-
code: 'SPX',
|
|
72
|
-
sortKey: 'marketValue',
|
|
73
|
-
sortType: 'desc',
|
|
74
|
-
style: 'tablelist',
|
|
75
|
-
finClientType: 'pc',
|
|
76
|
-
pn, rn,
|
|
77
|
-
};
|
|
78
|
-
try {
|
|
79
|
-
const resp = await this.get('https://finance.pae.baidu.com/sapi/v1/constituents', params);
|
|
80
|
-
const stocklist = resp.Result.list.body;
|
|
81
|
-
return stocklist.map(item => {
|
|
82
|
-
const { code: nc, name: n, rawData } = item;
|
|
83
|
-
const { lastPx: c, volume: v, marketValue: mv } = rawData;
|
|
84
|
-
return { nc, n, c, v, mv };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
throw new Error(`failed to load s&p ${err.message}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async sp500() {
|
|
92
|
-
let pn = 0, rn = 100;
|
|
93
|
-
let sts = [];
|
|
94
|
-
while (true) {
|
|
95
|
-
const _sts = await this.__sp500(pn, rn);
|
|
96
|
-
const _len = _sts.length;
|
|
97
|
-
sts = sts.concat(_sts);
|
|
98
|
-
if (_len < rn)
|
|
99
|
-
break;
|
|
100
|
-
pn += _len;
|
|
101
|
-
}
|
|
102
|
-
return sts;
|
|
103
|
-
}
|
|
104
|
-
async loadPrice(market) {
|
|
105
|
-
const stamp = getMarketCloseTime(EnMarket.USA);
|
|
106
|
-
let sts = Promise.resolve([]);
|
|
107
|
-
switch (market) {
|
|
108
|
-
case EnMarket.HKEX:
|
|
109
|
-
sts = this.hkex();
|
|
110
|
-
break;
|
|
111
|
-
case EnMarket.SGX:
|
|
112
|
-
sts = this.sgx();
|
|
113
|
-
break;
|
|
114
|
-
case EnMarket.USA:
|
|
115
|
-
sts = this.sp500();
|
|
116
|
-
break;
|
|
117
|
-
default:
|
|
118
|
-
throw new Error(`unsupported market ${market}`);
|
|
119
|
-
}
|
|
120
|
-
return { sts: await sts, stamp };
|
|
121
|
-
}
|
|
122
|
-
}
|
package/types/close-time.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
export declare const A_MIN_MS = 60000;
|
|
3
|
-
export declare const A_HOUR_MS = 3600000;
|
|
4
|
-
export declare const A_DAY_MS = 86400000;
|
|
5
|
-
/**
|
|
6
|
-
* 获取市场收盘时间戳,单位秒
|
|
7
|
-
* @param market 市场
|
|
8
|
-
* @param time 可选时间戳(毫秒),默认为当前时间,用于确定日期和夏令时状态
|
|
9
|
-
* @returns 市场收盘秒级时间戳
|
|
10
|
-
*/
|
|
11
|
-
export declare function getMarketCloseTime(market: EnMarket, time?: number): number;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import PriceLoader from './price-loader.js';
|
|
2
|
-
import { type IDiv } from './helper.js';
|
|
3
|
-
export declare class DividendLoader extends PriceLoader {
|
|
4
|
-
private __laodIbmCode;
|
|
5
|
-
private __loadHKDividend;
|
|
6
|
-
private __loadSGDividend;
|
|
7
|
-
private __loadUSDividend;
|
|
8
|
-
loadDividend(fullNc: string): Promise<{
|
|
9
|
-
fullNc: string;
|
|
10
|
-
nc: string;
|
|
11
|
-
dps: IDiv[];
|
|
12
|
-
bps: IDiv[];
|
|
13
|
-
}>;
|
|
14
|
-
}
|
package/types/helper.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { EnMarket } from '@watsonserve/stock-base';
|
|
2
|
-
export declare function sleep(t?: number): Promise<unknown>;
|
|
3
|
-
export declare function splitFullNc(fullNc: string): {
|
|
4
|
-
nc: string;
|
|
5
|
-
market: EnMarket | undefined;
|
|
6
|
-
};
|
|
7
|
-
export interface IDiv {
|
|
8
|
-
nc: string;
|
|
9
|
-
annc: number;
|
|
10
|
-
ex: number;
|
|
11
|
-
paid: number;
|
|
12
|
-
currency: string;
|
|
13
|
-
amount: number;
|
|
14
|
-
}
|
|
15
|
-
export declare function ttm<T>(by: string, list: any[]): T[];
|