@watsonserve/stock-base 0.1.4 → 0.1.6

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/history-loader.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EnMarket } from './stock.js';
1
+ import { EnCurrency, EnMarket } from './stock.js';
2
2
  import { getMarketCloseTime } from './close-time.js';
3
3
  import WebRequest from './web-request.js';
4
4
  import { sleep, splitFullNc } from './helper.js';
@@ -6,12 +6,12 @@ import { sleep, splitFullNc } from './helper.js';
6
6
  export default class HistoryLoader extends WebRequest {
7
7
  async __getHKOrUSAHistory(market, nc) {
8
8
  let cntry = EnMarket.USA === market ? 'us' : 'hk';
9
- await this.goto(`https://quote.eastmoney.com/${cntry}/${nc}.html`);
9
+ await super.goto(`https://quote.eastmoney.com/${cntry}/${nc}.html`);
10
10
  let secid = `116.${nc}`;
11
11
  if (EnMarket.USA === market) {
12
12
  await sleep(500);
13
13
  // secid = await this.get_em_code(nc);
14
- secid = await this.read('quotecode');
14
+ secid = await super.read('quotecode');
15
15
  }
16
16
  const params = {
17
17
  fields1: 'f1,f2,f3,f4,f5,f6',
@@ -24,7 +24,7 @@ export default class HistoryLoader extends WebRequest {
24
24
  // ut: 'bd1d9ddb04089700cf9c27f6f7426281',
25
25
  // lmt: 1000000,
26
26
  };
27
- if (this.isDebug) {
27
+ if (super.isDebug) {
28
28
  await sleep(5000);
29
29
  }
30
30
  const resp = await super.get('https://push2his.eastmoney.com/api/qt/stock/kline/get', params);
@@ -52,7 +52,10 @@ export default class HistoryLoader extends WebRequest {
52
52
  });
53
53
  }
54
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' })));
55
+ await super.goto('https://finance.baidu.com');
56
+ await sleep(500);
57
+ const fxs = await Promise.all([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY].map(code => super.parisFetch('https://finance.pae.baidu.com/vapi/v1/getquotation', 'GET', { group: 'huilv_kline', ktype: 'day', code: `USD${code}`, count: 204, finClientType: 'pc' })));
58
+ await super.goto('about:blank');
56
59
  const [hk, sg, cn] = fxs.map(body => body.Result.newMarketData.marketData.split(';'));
57
60
  const base = new Map(hk.map(line => {
58
61
  const [_0, date, _1, c] = line.split(',');
package/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "@watsonserve/stock-base",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "./types/index.d.ts",
8
- "scripts": {
9
- "build": "rm -rf ./dist && tsc && node ./build.mjs"
10
- },
11
8
  "keywords": [],
12
9
  "author": "",
13
10
  "license": "ISC",
package/price-loader.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { EnCurrency, EnMarket } from './stock.js';
2
2
  import { getMarketCloseTime } from './close-time.js';
3
3
  import HistoryLoader from './history-loader.js';
4
+ import { sleep } from './helper.js';
4
5
  export default class PriceLoader extends HistoryLoader {
5
6
  // 匯率
6
7
  async __loadFx(ncs) {
7
8
  try {
8
9
  const data = new Map();
9
10
  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];
11
+ const body = await this.parisFetch('https://finance.pae.baidu.com/vapi/v1/sug', 'GET', { wd: `USD${nc}`, skip_login: 1, finClientType: 'pc' });
12
+ const fx = body.Result.list[0];
12
13
  data.set(nc, +fx.price);
13
14
  }
14
15
  return Object.fromEntries(data.entries());
@@ -18,7 +19,10 @@ export default class PriceLoader extends HistoryLoader {
18
19
  }
19
20
  }
20
21
  async loadFx() {
22
+ await this.goto('https://finance.baidu.com');
23
+ await sleep(500);
21
24
  const fxs = await this.__loadFx([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY]);
25
+ await this.goto('about:blank');
22
26
  const stamp = getMarketCloseTime(EnMarket.FX);
23
27
  return { ...fxs, stamp };
24
28
  }
@@ -76,7 +80,7 @@ export default class PriceLoader extends HistoryLoader {
76
80
  pn, rn,
77
81
  };
78
82
  try {
79
- const resp = await this.get('https://finance.pae.baidu.com/sapi/v1/constituents', params);
83
+ const resp = await this.parisFetch('https://finance.pae.baidu.com/sapi/v1/constituents', 'GET', params);
80
84
  const stocklist = resp.Result.list.body;
81
85
  return stocklist.map(item => {
82
86
  const { code: nc, name: n, rawData } = item;
@@ -91,6 +95,8 @@ export default class PriceLoader extends HistoryLoader {
91
95
  async sp500() {
92
96
  let pn = 0, rn = 100;
93
97
  let sts = [];
98
+ await this.goto('https://finance.baidu.com');
99
+ await sleep(500);
94
100
  while (true) {
95
101
  const _sts = await this.__sp500(pn, rn);
96
102
  const _len = _sts.length;
@@ -99,6 +105,7 @@ export default class PriceLoader extends HistoryLoader {
99
105
  break;
100
106
  pn += _len;
101
107
  }
108
+ await this.goto('about:blank');
102
109
  return sts;
103
110
  }
104
111
  async loadPrice(market) {
@@ -17,6 +17,7 @@ export default class WebRequest {
17
17
  protected fetch(url: string, method?: string, params?: Record<string, any>, headers?: Record<string, any>): Promise<any>;
18
18
  protected get(url: string, params?: Record<string, any>, headers?: Record<string, any>): Promise<any>;
19
19
  protected goto(pageUrl: string): Promise<import("playwright").Response | null>;
20
+ protected parisFetch<T>(url: string, method?: string, params?: Record<string, any>): Promise<T>;
20
21
  protected read<T>(key: string): Promise<T>;
21
22
  protected _loadHkexToken(): Promise<string>;
22
23
  }
@@ -0,0 +1,17 @@
1
+ import { type IDiv, type IFx, type IStockPoint } from './stock.js';
2
+ export default class YahooFinanceLoader {
3
+ private getJson;
4
+ private getChart;
5
+ loadHistory(fullNc: string): Promise<IStockPoint[]>;
6
+ loadFxHistory(): Promise<{
7
+ result: IFx[];
8
+ remain: IFx[];
9
+ }>;
10
+ loadFx(): Promise<IFx>;
11
+ loadUSDividend(fullNc: string): Promise<{
12
+ fullNc: string;
13
+ nc: string;
14
+ dps: IDiv[];
15
+ bps: IDiv[];
16
+ }>;
17
+ }
package/us-holiday.js CHANGED
@@ -1,11 +1,4 @@
1
- import { A_DAY_S } from "./close-time.js";
2
- // ==================== 工具函数 ====================
3
- function formatDate(d) {
4
- const y = d.getFullYear();
5
- const m = String(d.getMonth() + 1).padStart(2, '0');
6
- const day = String(d.getDate()).padStart(2, '0');
7
- return `${y}${m}${day}`;
8
- }
1
+ import { A_DAY_S } from './close-time.js';
9
2
  // 获取第N个周X
10
3
  function getNthWeekday(y, m, n, weekday) {
11
4
  const first = new Date(y, m - 1, 1);
@@ -42,17 +35,6 @@ function getGoodFriday(y) {
42
35
  goodFriday.setDate(easter.getDate() - 2);
43
36
  return goodFriday;
44
37
  }
45
- function createEvent(uid, dateStr, summary, description) {
46
- return `BEGIN:VEVENT
47
- UID:${uid}-${dateStr}@usstockmarket
48
- DTSTART;VALUE=DATE:${dateStr}
49
- DTEND;VALUE=DATE:${dateStr}
50
- SUMMARY:${summary}
51
- DESCRIPTION:${description}
52
- STATUS:CONFIRMED
53
- END:VEVENT
54
- `;
55
- }
56
38
  function toUTCs(d) {
57
39
  return ~~(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 1000);
58
40
  }
package/web-request.js CHANGED
@@ -102,6 +102,12 @@ export default class WebRequest {
102
102
  goto(pageUrl) {
103
103
  return this.page.goto(pageUrl);
104
104
  }
105
+ async parisFetch(url, method = 'GET', params) {
106
+ if (new URL(this.page.url()).origin !== 'https://finance.baidu.com')
107
+ throw new Error('Not on finance.baidu.com');
108
+ const token = await this.page.evaluate(() => new Promise(r => window['paris_2108'].getAcsToken(r)));
109
+ return this.fetch(url, method, params, { 'Acs-Token': token });
110
+ }
105
111
  read(key) {
106
112
  return this.page.evaluate(async (conf) => {
107
113
  return window[conf.key];
@@ -0,0 +1,168 @@
1
+ import { getMarketCloseTime } from './close-time.js';
2
+ import { splitFullNc, ttm } from './helper.js';
3
+ import { EnCurrency, EnMarket } from './stock.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
+ amount: div.amount || 0,
13
+ };
14
+ }
15
+ function toYahooSymbol(nc, market) {
16
+ switch (market) {
17
+ case EnMarket.USA:
18
+ return nc.replace(/\./g, '-');
19
+ case EnMarket.HKEX:
20
+ return `${nc.padStart(4, '0')}.HK`;
21
+ case EnMarket.SGX:
22
+ return `${nc}.SI`;
23
+ default:
24
+ }
25
+ throw new Error(`unsupported market ${market}`);
26
+ }
27
+ function getMarketFromCode(code) {
28
+ return {
29
+ US: EnMarket.USA,
30
+ HK: EnMarket.HKEX,
31
+ SG: EnMarket.SGX,
32
+ }[code];
33
+ }
34
+ export default class YahooFinanceLoader {
35
+ async getJson(url, params) {
36
+ const uri = new URL(url);
37
+ if (params) {
38
+ uri.search = new URLSearchParams(Object.entries(params).reduce((pre, [k, v]) => {
39
+ pre[k] = `${v}`;
40
+ return pre;
41
+ }, {})).toString();
42
+ }
43
+ const resp = await fetch(uri.toString());
44
+ if (!resp.ok)
45
+ throw new Error(`HTTP ${resp.status}`);
46
+ return await resp.json();
47
+ }
48
+ async getChart(symbol, params) {
49
+ const resp = await this.getJson(`https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}`, params);
50
+ const error = resp.chart?.error;
51
+ if (error?.description) {
52
+ throw new Error(error.description);
53
+ }
54
+ const data = resp.chart?.result?.[0];
55
+ if (!data)
56
+ throw new Error(`empty chart data for ${symbol}`);
57
+ return data;
58
+ }
59
+ async loadHistory(fullNc) {
60
+ const { nc, market } = splitFullNc(fullNc);
61
+ if (!market)
62
+ throw new Error(`unsupported market in ${fullNc}`);
63
+ const symbol = toYahooSymbol(nc, market);
64
+ const chart = await this.getChart(symbol, { range: 'max', interval: '1d', events: 'history' });
65
+ const timestamps = chart.timestamp || [];
66
+ const quote = chart.indicators?.quote?.[0];
67
+ if (!quote)
68
+ return [];
69
+ return timestamps.reduce((pre, ts, idx) => {
70
+ const c = quote.close?.[idx];
71
+ if ('number' !== typeof c)
72
+ return pre;
73
+ pre.push({
74
+ market,
75
+ timestamp: getMarketCloseTime(market, ts * 1000),
76
+ st: {
77
+ nc,
78
+ n: '',
79
+ o: quote.open?.[idx] || 0,
80
+ c,
81
+ h: quote.high?.[idx] || c,
82
+ l: quote.low?.[idx] || c,
83
+ v: quote.volume?.[idx] || 0,
84
+ },
85
+ });
86
+ return pre;
87
+ }, []);
88
+ }
89
+ async loadFxHistory() {
90
+ const symbols = [
91
+ { symbol: 'USDHKD=X', currency: EnCurrency.HKD },
92
+ { symbol: 'USDSGD=X', currency: EnCurrency.SGD },
93
+ { symbol: 'USDCNY=X', currency: EnCurrency.CNY },
94
+ ];
95
+ const historyList = await Promise.all(symbols.map(async ({ symbol, currency }) => {
96
+ const chart = await this.getChart(symbol, { range: 'max', interval: '1d', events: 'history' });
97
+ const timestamps = chart.timestamp || [];
98
+ const quote = chart.indicators?.quote?.[0];
99
+ const close = quote?.close || [];
100
+ return { currency, lines: timestamps.map((ts, idx) => ({ ts, c: close[idx] })) };
101
+ }));
102
+ const base = new Map();
103
+ historyList.forEach(({ currency, lines }) => {
104
+ lines.forEach(({ ts, c }) => {
105
+ if ('number' !== typeof c)
106
+ return;
107
+ const stamp = getMarketCloseTime(EnMarket.FX, ts * 1000);
108
+ const fx = base.get(stamp) || {};
109
+ fx[currency] = c;
110
+ base.set(stamp, fx);
111
+ });
112
+ });
113
+ const result = [];
114
+ const remain = [];
115
+ [...base.entries()].forEach(([stamp, fx]) => {
116
+ const data = { stamp, ...fx };
117
+ if (!fx.HKD || !fx.SGD || !fx.CNY) {
118
+ remain.push(data);
119
+ }
120
+ else {
121
+ result.push(data);
122
+ }
123
+ });
124
+ return { result, remain };
125
+ }
126
+ async loadFx() {
127
+ const resp = await this.getJson('https://query1.finance.yahoo.com/v7/finance/quote', { symbols: 'USDHKD=X,USDSGD=X,USDCNY=X' });
128
+ const rows = resp.quoteResponse?.result || [];
129
+ const values = rows.reduce((pre, item) => {
130
+ const sym = item.symbol || '';
131
+ const nc = sym.slice(3, 6);
132
+ if (item.regularMarketPrice)
133
+ pre[nc] = item.regularMarketPrice;
134
+ return pre;
135
+ }, {});
136
+ return {
137
+ stamp: getMarketCloseTime(EnMarket.FX),
138
+ HKD: values.HKD || 0,
139
+ SGD: values.SGD || 0,
140
+ CNY: values.CNY || 0,
141
+ };
142
+ }
143
+ async loadUSDividend(fullNc) {
144
+ const marketCode = fullNc.substring(fullNc.length - 2);
145
+ if ('US' !== marketCode)
146
+ throw new Error(`unsupported market in ${fullNc}`);
147
+ const nc = fullNc.substring(0, fullNc.length - 3);
148
+ const market = getMarketFromCode(marketCode);
149
+ const symbol = toYahooSymbol(nc, market);
150
+ const chart = await this.getChart(symbol, { range: 'max', interval: '1d', events: 'div' });
151
+ const dividendMap = chart.events?.dividends || {};
152
+ const dividends = Object.values(dividendMap)
153
+ .sort((a, b) => (b.date || 0) - (a.date || 0))
154
+ .map(item => {
155
+ const ts = (item.date || 0) * 1000;
156
+ const date = new Date(ts);
157
+ return {
158
+ currency: EnCurrency.USD,
159
+ amount: item.amount || 0,
160
+ annc: date,
161
+ ex: date,
162
+ paid: date,
163
+ };
164
+ });
165
+ const dps = ttm('ex', dividends).map(item => toDivPoint(fullNc, item));
166
+ return { fullNc, nc, dps, bps: [] };
167
+ }
168
+ }