@watsonserve/stock-base 0.1.5 → 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.
@@ -1,167 +0,0 @@
1
- import { log } from './log.js';
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
- function dateCNtoDate(str) {
16
- const ymd = str.match(rxDate)?.slice(0, 3).map(i => +i);
17
- if (!ymd)
18
- return null;
19
- if ('number' === typeof ymd?.[1]) {
20
- ymd[1] -= 1;
21
- }
22
- return new Date(Date.UTC.apply(null, ymd));
23
- }
24
- export class DividendLoader extends PriceLoader {
25
- async __laodIbmCode(nc) {
26
- const resp = await super.get(`https://api.sgx.com/marketmetadata/v2?stock-code=${nc}`);
27
- return resp.data?.[0]?.ibmCode || '';
28
- }
29
- async __loadHKDividend(nc) {
30
- const token = await super._loadHkexToken();
31
- const qid = Date.now();
32
- const callback = `jsonp_${qid}`;
33
- const params = { sym: +nc, token, lang: 'chi', recperpage: 5, page: 1, qid, callback };
34
- const resp = await super.get('https://www1.hkex.com.hk/hkexwidget/data/getequityentitlement', params);
35
- const strJson = resp.substring(callback.length + 1, resp.length - 1);
36
- const { entitlementlist } = JSON.parse(strJson).data;
37
- const list = [];
38
- for (const item of entitlementlist) {
39
- const { detail, announcement_date, ex_date, payment_date, announcement_URL } = item;
40
- const annc = dateCNtoDate(announcement_date);
41
- const ex = dateCNtoDate(ex_date);
42
- const paid = dateCNtoDate(payment_date);
43
- const divid = Array.from(detail.match(/(USD|HKD|GBP) (\d+\.\d+)/g) || [])
44
- .reduce((pre, item) => {
45
- const [currency, amount] = item.split(' ');
46
- pre[currency] = { currency, amount: +amount };
47
- return pre;
48
- }, {});
49
- list.push({ ...(divid['HKD'] || divid['USD'] || divid['GBP']), annc, ex, paid, announcement_URL });
50
- }
51
- return [ttm('ex', list), []];
52
- }
53
- async __loadSGDividend(nc) {
54
- const ibmcode = await this.__laodIbmCode(nc);
55
- const params = {
56
- pagesize: 15,
57
- pagestart: 0,
58
- ibmcode,
59
- params: 'id,anncType,dateAnnc,exDate,name,particulars,recDate,datePaid',
60
- order: 'desc',
61
- orderBy: 'dateAnnc'
62
- };
63
- const { data } = await super.get('https://api.sgx.com/corporateactions/v1.0', params);
64
- const bonus = [];
65
- const dividend = [];
66
- for (const item of data) {
67
- const { anncType, particulars, dateAnnc, exDate, recDate, datePaid } = item;
68
- const annc = new Date(dateAnnc);
69
- const ex = new Date(exDate);
70
- const rec = new Date(recDate); // 登记日
71
- const paid = new Date(datePaid);
72
- if ('DIVIDEND' === anncType) {
73
- const amount = +particulars.match(/SGD (\d+\.\d+)/)?.[1] || 0;
74
- const lstIdx = dividend.length - 1;
75
- if (dividend[lstIdx]?.ex.getTime() === ex.getTime() && amount) {
76
- dividend[lstIdx].amount += amount;
77
- }
78
- else {
79
- dividend.push({ currency: 'SGD', amount, annc, ex, rec, paid });
80
- }
81
- continue;
82
- }
83
- if ('BONUS' === anncType) { // particulars: 'Ratio: 10:1'
84
- const ratio = particulars.match(/Ratio: (\d+:\d+)/);
85
- const [foo, bar] = ratio[1].split(':');
86
- bonus.push({ currency: 'SGD', ratio: 1 + (+bar) / (+foo), annc, ex, rec, paid });
87
- continue;
88
- }
89
- }
90
- return [ttm('ex', dividend), bonus];
91
- }
92
- async __loadUSDividend(nc) {
93
- const params = {
94
- reportName: 'RPT_USF10_INFO_DIVIDEND',
95
- 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',
96
- // filter: `(SECUCODE="${nc}.N")`,
97
- filter: `(SECURITY_CODE="${nc.replace('.', '_')}")`,
98
- pageNumber: 1,
99
- pageSize: 200,
100
- sortTypes: -1,
101
- sortColumns: 'EX_DIVIDEND_DATE',
102
- source: 'SECURITIES',
103
- client: 'PC',
104
- v: Math.random().toString().substring(2).padStart(17, '0')
105
- };
106
- const strResp = await super.get('https://datacenter.eastmoney.com/securities/api/data/v1/get', params);
107
- const data = (JSON.parse(strResp).result?.data || []);
108
- const dividend = [];
109
- for (const item of data) {
110
- const { ASSIGN_TYPE, PLAN_EXPLAIN, NOTICE_DATE, EX_DIVIDEND_DATE, BONUS_PAY_DATE } = item;
111
- if ('Cash' === ASSIGN_TYPE || 'Special cash' === ASSIGN_TYPE) {
112
- const amount = +PLAN_EXPLAIN.match(/(\d*\.?\d+)美元/)[1]; // 每1股派0.82美元股息
113
- if (!amount)
114
- console.warn(item);
115
- dividend.push({
116
- currency: 'USD',
117
- amount,
118
- annc: new Date(NOTICE_DATE),
119
- ex: new Date(EX_DIVIDEND_DATE),
120
- paid: new Date(BONUS_PAY_DATE),
121
- });
122
- continue;
123
- }
124
- }
125
- return [ttm('ex', dividend), []];
126
- }
127
- async loadDividend(fullNc) {
128
- const nc = fullNc.substring(0, fullNc.length - 3);
129
- const market = fullNc.substring(fullNc.length - 2);
130
- let data;
131
- log.info(`start loading ${fullNc} dividend...`);
132
- switch (market) {
133
- case 'SG':
134
- data = await this.__loadSGDividend(nc);
135
- break;
136
- case 'HK':
137
- data = await this.__loadHKDividend(nc);
138
- break;
139
- case 'US':
140
- data = await this.__loadUSDividend(nc);
141
- break;
142
- default:
143
- throw new Error(`Unsupported market: ${market}`);
144
- }
145
- const [div, bonus] = data;
146
- const dps = div.map(item => toDivPoint(fullNc, item));
147
- const bps = bonus.map(item => toDivPoint(fullNc, item));
148
- log.info(`finished loading ${fullNc} dividend`);
149
- return { fullNc, nc, dps, bps };
150
- }
151
- async loadDividends(ncs) {
152
- const ign = [];
153
- let points = [];
154
- const divPool = {};
155
- const divs = await Promise.all(ncs.map(fullNc => this.loadDividend(fullNc)));
156
- for (const { fullNc, nc, dps, bps } of divs) {
157
- if (!dps.length && !bps.length) {
158
- ign.push(fullNc);
159
- }
160
- else {
161
- points = points.concat(dps, bps);
162
- }
163
- divPool[nc] = ttm('ex', dps);
164
- }
165
- return { ign, points, divPool };
166
- }
167
- }
package/history-loader.js DELETED
@@ -1,100 +0,0 @@
1
- import { EnMarket } from './stock.js';
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,
37
- timestamp: getMarketCloseTime(market, new Date(`${date}T15:00:00Z`).getTime()),
38
- st: { nc, n: '', o: +open, c: +close, h: +hight, l: +low, v: +vol }
39
- };
40
- });
41
- }
42
- async __getSGXHistory(nc) {
43
- const rsp = await super.get(`https://api.sgx.com/securities/v1.1/charts/historic/stocks/code/${nc}/5y`, { params: 'trading_time,vl,lt' });
44
- const { historic } = rsp.data;
45
- return historic.map(st => {
46
- const { trading_time, vl: v, lt: c } = st;
47
- const fullYear = trading_time.substring(0, 4);
48
- const month = trading_time.substring(4, 6);
49
- const date = trading_time.substring(6, 8);
50
- const timestamp = ~~(new Date(`${fullYear}-${month}-${date}T09:16:00Z`).getTime() / 1000);
51
- return { market: EnMarket.SGX, timestamp, st: { nc, n: '', o: 0, c, h: 0, l: 0, v } };
52
- });
53
- }
54
- async loadFxHistory() {
55
- const fxs = await Promise.all(['USDHKD', 'USDSGD', 'USDCNY'].map(code => this.get('https://finance.pae.baidu.com/vapi/v1/getquotation', { group: 'huilv_kline', ktype: 'day', code, finClientType: 'pc' })));
56
- const [hk, sg, cn] = fxs.map(body => body.Result.newMarketData.marketData.split(';'));
57
- const base = new Map(hk.map(line => {
58
- const [_0, date, _1, c] = line.split(',');
59
- const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
60
- return [stamp, { date, HKD: +c }];
61
- }));
62
- sg.forEach(line => {
63
- const [_0, date, _1, c] = line.split(',');
64
- const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
65
- const fx = base.get(stamp) || { date };
66
- fx.SGD = +c;
67
- base.set(stamp, fx);
68
- });
69
- cn.map(line => {
70
- const [_0, date, _1, c] = line.split(',');
71
- const stamp = getMarketCloseTime(EnMarket.FX, new Date(`${date}T15:00:00Z`).getTime());
72
- const fx = base.get(stamp) || { date };
73
- fx.CNY = +c;
74
- base.set(stamp, fx);
75
- });
76
- const result = [];
77
- const remain = [];
78
- [...base.entries()].forEach(([stamp, fx]) => {
79
- const _fx = { stamp, ...fx };
80
- if (!fx.HKD || !fx.SGD || !fx.CNY) {
81
- remain.push(_fx);
82
- return;
83
- }
84
- result.push(_fx);
85
- });
86
- return { result, remain };
87
- }
88
- loadHistory(fullNc) {
89
- const { nc, market } = splitFullNc(fullNc);
90
- switch (market) {
91
- case EnMarket.SGX:
92
- return this.__getSGXHistory(nc);
93
- case EnMarket.HKEX:
94
- case EnMarket.USA:
95
- return this.__getHKOrUSAHistory(market, nc);
96
- default:
97
- }
98
- throw new Error(`Unsupported market: ${market}`);
99
- }
100
- }
package/price-loader.js DELETED
@@ -1,122 +0,0 @@
1
- import { EnCurrency, EnMarket } from './stock.js';
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/vapi/v1/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(market);
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
- }
@@ -1,19 +0,0 @@
1
- import PriceLoader from './price-loader.js';
2
- import { type IDiv } from './stock.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
- loadDividends(ncs: string[]): Promise<{
15
- ign: string[];
16
- points: IDiv[];
17
- divPool: Record<string, IDiv[]>;
18
- }>;
19
- }
@@ -1,11 +0,0 @@
1
- import { IFx, type IStockPoint } from './stock.js';
2
- import WebRequest from './web-request.js';
3
- export default class HistoryLoader extends WebRequest {
4
- private __getHKOrUSAHistory;
5
- private __getSGXHistory;
6
- loadFxHistory(): Promise<{
7
- result: IFx[];
8
- remain: IFx[];
9
- }>;
10
- loadHistory(fullNc: string): Promise<IStockPoint[]>;
11
- }
@@ -1,14 +0,0 @@
1
- import { EnMarket, type IFx, IStock } from './stock.js';
2
- import HistoryLoader from './history-loader.js';
3
- export default class PriceLoader extends HistoryLoader {
4
- private __loadFx;
5
- loadFx(): Promise<IFx>;
6
- protected sgx(): Promise<IStock[]>;
7
- protected hkex(): Promise<IStock[]>;
8
- private __sp500;
9
- protected sp500(): Promise<IStock[]>;
10
- loadPrice(market: EnMarket): Promise<{
11
- sts: IStock[];
12
- stamp: number;
13
- }>;
14
- }