@watsonserve/stock-loader 0.0.2-alpha.1 → 0.0.2-alpha.2

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,125 +0,0 @@
1
- import { EnCurrency, EnMarket, IStock } from '@watsonserve/stock-base';
2
- import { getMarketCloseTime } from './close-time.js';
3
- import HistoryLoader from './history-loader.js';
4
-
5
- export default class PriceLoader extends HistoryLoader {
6
- // 匯率
7
- private async __loadFx(ncs: EnCurrency[]) {
8
- try {
9
- const data = new Map<EnCurrency, number>();
10
- for (const nc of ncs) {
11
- const body = await this.get('https://finance.pae.baidu.com/selfselect/sug', {wd: `USD${nc}`, skip_login: 1, finClientType: 'pc'});
12
- const fx = body.Result.stock[0];
13
- data.set(nc, +fx.price);
14
- }
15
- return Object.fromEntries(data.entries());
16
- } catch (err) {
17
- throw new Error(`failed to load fx ${(err as Error).message}`);
18
- }
19
- }
20
-
21
- async loadFx() {
22
- const fxs = await this.__loadFx([EnCurrency.SGD, EnCurrency.HKD, EnCurrency.CNY]);
23
- const stamp = getMarketCloseTime(EnMarket.FX);
24
- return { ...fxs, stamp };
25
- }
26
-
27
- // 新加坡交易所全量股票
28
- protected async sgx() {
29
- try {
30
- const body = await this.get('https://api.sgx.com/securities/v1.1/stocks', { params: 'nc,lt,ptd,n,o,h,l,vl,trading_time' });
31
- const stockList = body.data.prices as any[];
32
- return stockList.map(({ trading_time:t,nc,lt:c,n,o,h,l,vl:v }) => ({ t, nc,n,o,c,h,l,v } as IStock));
33
- } catch (err) {
34
- throw new Error(`failed to load sgx ${(err as Error).message}`);
35
- }
36
- }
37
-
38
- // 香港交易所主板股票
39
- protected async hkex() {
40
- try {
41
- const token = await this._loadHkexToken();
42
- const qid = Date.now();
43
- const callback = `jsonp_${qid}`;
44
- const params = {
45
- lang: 'chi',
46
- token,
47
- sort: 5,
48
- order: 0,
49
- all: 1,
50
- subcat: 1,
51
- market: 'MAIN',
52
- qid,
53
- callback,
54
- };
55
- const resp = await this.get('https://www1.hkex.com.hk/hkexwidget/data/getequityfilter', params);
56
- const strJson = resp.substring(callback.length + 1, resp.length - 1);
57
- const body = JSON.parse(strJson);
58
- const { stocklist } = body.data;
59
- return (stocklist as any[]).map(item => {
60
- const { sym, nm: n, ls } = item;
61
- return { nc: sym.padStart(5, '0'), n, c: +ls } as IStock;
62
- });
63
- } catch (err) {
64
- throw new Error(`failed to load hkex ${(err as Error).message}`);
65
- }
66
- }
67
-
68
- // 标普500成份股
69
- private async __sp500(pn = 0, rn = 100) {
70
- const params = {
71
- financeType: 'index',
72
- market: 'us',
73
- code: 'SPX',
74
- sortKey: 'marketValue',
75
- sortType: 'desc',
76
- style: 'tablelist',
77
- finClientType: 'pc',
78
- pn, rn,
79
- };
80
-
81
- try {
82
- const resp = await this.get('https://finance.pae.baidu.com/sapi/v1/constituents', params);
83
- const stocklist = resp.Result.list.body as any[];
84
- return stocklist.map(item => {
85
- const { code: nc, name: n, rawData } = item;
86
- const { lastPx: c, volume: v, marketValue: mv } = rawData;
87
- return { nc, n, c, v, mv } as IStock;
88
- });
89
- } catch (err) {
90
- throw new Error(`failed to load s&p ${(err as Error).message}`);
91
- }
92
- }
93
-
94
- protected async sp500() {
95
- let pn = 0, rn = 100;
96
- let sts: IStock[] = [];
97
- while (true) {
98
- const _sts = await this.__sp500(pn, rn);
99
- const _len = _sts.length;
100
- sts = sts.concat(_sts);
101
- if (_len < rn) break;
102
- pn += _len;
103
- }
104
- return sts;
105
- }
106
-
107
- async loadPrice(market: EnMarket) {
108
- const stamp = getMarketCloseTime(EnMarket.USA);
109
- let sts = Promise.resolve<IStock[]>([]);
110
- switch (market) {
111
- case EnMarket.HKEX:
112
- sts = this.hkex();
113
- break;
114
- case EnMarket.SGX:
115
- sts = this.sgx();
116
- break;
117
- case EnMarket.USA:
118
- sts = this.sp500();
119
- break;
120
- default:
121
- throw new Error(`unsupported market ${market}`);
122
- }
123
- return { sts: await sts, stamp };
124
- }
125
- }
@@ -1,131 +0,0 @@
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 DELETED
@@ -1,22 +0,0 @@
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
- }