@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/sgx.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { A_DAY_S } from './close-time.js';
|
|
2
|
+
import { formatFullNc, parseUTCDate, ttm } from './helper.js';
|
|
3
|
+
import { EnMarket } from './stock.js';
|
|
4
|
+
export class SGXLoader {
|
|
5
|
+
async __fetch(url, params) {
|
|
6
|
+
const uri = new URL(url);
|
|
7
|
+
const urlParams = Object.fromEntries(uri.searchParams);
|
|
8
|
+
uri.search = new URLSearchParams(Object.assign(urlParams, params)).toString();
|
|
9
|
+
const resp = await fetch(uri.toString(), {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
credentials: 'include'
|
|
12
|
+
});
|
|
13
|
+
if (!resp.ok)
|
|
14
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
15
|
+
const ct = resp.headers.get('Content-Type')?.split(';')[0] || '';
|
|
16
|
+
if ('application/json' === ct)
|
|
17
|
+
return resp.json();
|
|
18
|
+
if (ct.startsWith('text/'))
|
|
19
|
+
return resp.text();
|
|
20
|
+
return resp.arrayBuffer();
|
|
21
|
+
}
|
|
22
|
+
async __laodIbmCode(nc) {
|
|
23
|
+
const resp = await this.__fetch(`https://api.sgx.com/marketmetadata/v2`, { 'stock-code': nc });
|
|
24
|
+
return resp.data?.[0]?.ibmCode || '';
|
|
25
|
+
}
|
|
26
|
+
async loadDividend(nc) {
|
|
27
|
+
const ibmcode = await this.__laodIbmCode(nc);
|
|
28
|
+
const params = {
|
|
29
|
+
pagesize: 15,
|
|
30
|
+
pagestart: 0,
|
|
31
|
+
ibmcode,
|
|
32
|
+
params: 'id,anncType,dateAnnc,exDate,name,particulars,recDate,datePaid',
|
|
33
|
+
order: 'desc',
|
|
34
|
+
orderBy: 'dateAnnc'
|
|
35
|
+
};
|
|
36
|
+
const { data } = await this.__fetch('https://api.sgx.com/corporateactions/v1.0', params);
|
|
37
|
+
const bonus = [];
|
|
38
|
+
const dividend = [];
|
|
39
|
+
for (const item of data) {
|
|
40
|
+
const { anncType, particulars, dateAnnc, exDate, recDate, datePaid } = item;
|
|
41
|
+
const annc = new Date(dateAnnc);
|
|
42
|
+
const ex = new Date(exDate);
|
|
43
|
+
const rec = new Date(recDate); // 登记日
|
|
44
|
+
const paid = new Date(datePaid);
|
|
45
|
+
if ('DIVIDEND' === anncType) {
|
|
46
|
+
const amount = +particulars.match(/SGD (\d+\.\d+)/)?.[1] || 0;
|
|
47
|
+
const lstIdx = dividend.length - 1;
|
|
48
|
+
if (dividend[lstIdx]?.ex.getTime() === ex.getTime() && amount) {
|
|
49
|
+
dividend[lstIdx].amount += amount;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
dividend.push({ currency: 'SGD', amount, annc, ex, rec, paid });
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if ('BONUS' === anncType) { // particulars: 'Ratio: 10:1'
|
|
57
|
+
const ratio = particulars.match(/Ratio: (\d+:\d+)/);
|
|
58
|
+
const [foo, bar] = ratio[1].split(':');
|
|
59
|
+
bonus.push({ currency: 'SGD', ratio: 1 + (+bar) / (+foo), annc, ex, rec, paid });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return formatFullNc(nc, 'SG', ttm('ex', dividend), bonus);
|
|
64
|
+
}
|
|
65
|
+
async loadHistory(nc, int = '5y') {
|
|
66
|
+
const rsp = await this.__fetch(`https://api.sgx.com/securities/v1.1/charts/historic/stocks/code/${nc}/5y`, { params: 'trading_time,vl,lt' });
|
|
67
|
+
const { historic } = rsp.data;
|
|
68
|
+
return historic.map(st => {
|
|
69
|
+
const { trading_time, vl: v, lt: c } = st;
|
|
70
|
+
const fullYear = trading_time.substring(0, 4);
|
|
71
|
+
const month = trading_time.substring(4, 6);
|
|
72
|
+
const date = trading_time.substring(6, 8);
|
|
73
|
+
const timestamp = ~~(new Date(`${fullYear}-${month}-${date}T09:16:00Z`).getTime() / 1000);
|
|
74
|
+
return { market: EnMarket.SGX, timestamp, st: { nc, n: '', o: 0, c, h: 0, l: 0, v } };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// 新加坡交易所全量股票
|
|
78
|
+
async loadPrices() {
|
|
79
|
+
try {
|
|
80
|
+
const body = await this.__fetch('https://api.sgx.com/securities/v1.1/stocks', { params: 'nc,lt,ptd,n,o,h,l,vl,trading_time' });
|
|
81
|
+
const stockList = body.data.prices;
|
|
82
|
+
return stockList.map(({ trading_time: t, nc, lt: c, n, o, h, l, vl: v }) => ({ t, nc, n, o, c, h, l, v }));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new Error(`failed to load sgx ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async loadHolidays(year = 0) {
|
|
89
|
+
if (!year) {
|
|
90
|
+
year = new Date().getUTCFullYear();
|
|
91
|
+
}
|
|
92
|
+
const resp = await fetch(`https://www.mom.gov.sg/-/media/mom/documents/employment-practices/public-holidays/public-holidays-sg-${year}.ics`);
|
|
93
|
+
const text = (await resp.text()).replace(/\r\n/g, '\n').replace(/\n\n/g, '\n'); // handle line folding
|
|
94
|
+
return text.split('\nEND:VEVENT\nBEGIN:VEVENT\n').reduce((pre, blk) => {
|
|
95
|
+
const ev = new Map(blk.split('\n').map(line => line.split(':', 2)));
|
|
96
|
+
let start = parseUTCDate(ev.get('DTSTART;VALUE=DATE') || '');
|
|
97
|
+
let end = parseUTCDate(ev.get('DTEND;VALUE=DATE') || '');
|
|
98
|
+
if (start === end) {
|
|
99
|
+
end += A_DAY_S;
|
|
100
|
+
}
|
|
101
|
+
// recording to SG calendar, we will move it to next day(Monday, 1) if holiday is Sunday(0)
|
|
102
|
+
if (!new Date(start * 1000).getUTCDay()) {
|
|
103
|
+
end += A_DAY_S;
|
|
104
|
+
}
|
|
105
|
+
const firstOne = pre[0];
|
|
106
|
+
const title = ev.get('SUMMARY') || '';
|
|
107
|
+
(firstOne && end === firstOne.start) ? Object.assign(firstOne, { title, start }) : pre.unshift({ market: 'SG', title, start, end });
|
|
108
|
+
return pre;
|
|
109
|
+
}, []);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/types/baidu.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IFx, IStock } from './stock.js';
|
|
2
|
+
import WebRequest from './web-request.js';
|
|
3
|
+
export interface IFXs {
|
|
4
|
+
result: IFx[];
|
|
5
|
+
remain: IFx[];
|
|
6
|
+
}
|
|
7
|
+
export declare class BaiduLoader extends WebRequest {
|
|
8
|
+
private static __instance;
|
|
9
|
+
private __fetch;
|
|
10
|
+
private __loadFx;
|
|
11
|
+
private __sp500;
|
|
12
|
+
private static factory;
|
|
13
|
+
static getInstance(): Promise<BaiduLoader>;
|
|
14
|
+
private constructor();
|
|
15
|
+
protected _mount(): Promise<void>;
|
|
16
|
+
destroy(): Promise<void>;
|
|
17
|
+
loadFxHistory(): Promise<IFXs>;
|
|
18
|
+
loadFx(): Promise<IFx>;
|
|
19
|
+
loadPrices(): Promise<IStock[]>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IStockPoint, IDivBonInfo } from './stock.js';
|
|
2
|
+
import WebRequest from './web-request.js';
|
|
3
|
+
export declare class EastmoneyLoader extends WebRequest {
|
|
4
|
+
private static __instance;
|
|
5
|
+
private static factory;
|
|
6
|
+
static getInstance(): Promise<EastmoneyLoader>;
|
|
7
|
+
destroy(): Promise<void>;
|
|
8
|
+
private constructor();
|
|
9
|
+
loadDividend(nc: string): Promise<IDivBonInfo>;
|
|
10
|
+
/**
|
|
11
|
+
* @warning This method requires the Eastmoney page to be loaded and may not work if the page structure changes.
|
|
12
|
+
*/
|
|
13
|
+
loadHistory(nc: string): Promise<IStockPoint[]>;
|
|
14
|
+
}
|
package/types/helper.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnMarket } from './stock.js';
|
|
1
|
+
import { EnMarket, IDiv, IDivBonInfo, IDividend } from './stock.js';
|
|
2
2
|
export declare function sleep(t?: number): Promise<unknown>;
|
|
3
3
|
export declare function splitFullNc(fullNc: string): {
|
|
4
4
|
nc: string;
|
|
@@ -6,3 +6,6 @@ export declare function splitFullNc(fullNc: string): {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function groupBy<T extends Record<string, any>>(key: string, list: T[], delKey?: boolean): Record<string, T[]>;
|
|
8
8
|
export declare function ttm<T>(by: string, list: any[]): T[];
|
|
9
|
+
export declare function toDivPoint(fullNc: string, div: IDividend): IDiv;
|
|
10
|
+
export declare function parseUTCDate(str: string): number;
|
|
11
|
+
export declare function formatFullNc(nc: string, market: 'SG' | 'HK' | 'US', div: IDividend[], bonus: IDividend[]): IDivBonInfo;
|
package/types/hkex.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import WebRequest from './web-request.js';
|
|
2
|
+
import { EnMarket, IDivBonInfo, IHoliday, IStock } from './stock.js';
|
|
3
|
+
export declare enum EnInt {
|
|
4
|
+
day_1 = 0,
|
|
5
|
+
mo_1 = 2,
|
|
6
|
+
mo_3 = 3,
|
|
7
|
+
mo_6 = 4,
|
|
8
|
+
yr_1 = 5,
|
|
9
|
+
yrs_2 = 6,
|
|
10
|
+
yrs_5 = 7,
|
|
11
|
+
yrs_10 = 8,
|
|
12
|
+
ytd = 9
|
|
13
|
+
}
|
|
14
|
+
export declare enum EnSpan {
|
|
15
|
+
min = 0,
|
|
16
|
+
day = 6,
|
|
17
|
+
week = 7,
|
|
18
|
+
month = 8,
|
|
19
|
+
quarter = 9
|
|
20
|
+
}
|
|
21
|
+
export declare class HKEXLoader extends WebRequest {
|
|
22
|
+
private static HKEX_TOKEN;
|
|
23
|
+
private static tokenPromise;
|
|
24
|
+
private static __instance;
|
|
25
|
+
private __fetchHkexToken;
|
|
26
|
+
private __loadHkexToken;
|
|
27
|
+
private static factory;
|
|
28
|
+
static getInstance(): Promise<HKEXLoader>;
|
|
29
|
+
private constructor();
|
|
30
|
+
destroy(): Promise<void>;
|
|
31
|
+
loadDividend(nc: string): Promise<IDivBonInfo>;
|
|
32
|
+
loadPrices(): Promise<IStock[]>;
|
|
33
|
+
loadHolidays(): Promise<IHoliday[]>;
|
|
34
|
+
loadHistory(nc: string, int?: EnInt): Promise<{
|
|
35
|
+
market: EnMarket;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
st: {
|
|
38
|
+
nc: string;
|
|
39
|
+
n: string;
|
|
40
|
+
o: number;
|
|
41
|
+
h: number;
|
|
42
|
+
l: number;
|
|
43
|
+
c: number;
|
|
44
|
+
v: number;
|
|
45
|
+
};
|
|
46
|
+
}[]>;
|
|
47
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { IHoliday } from './stock.js';
|
|
1
|
+
import { IFXs } from './baidu.js';
|
|
2
|
+
import { EnMarket, IDiv, IFx, IHoliday, IStock, IStockPoint } from './stock.js';
|
|
3
3
|
export * from './close-time.js';
|
|
4
4
|
export * from './stock.js';
|
|
5
5
|
export * from './log.js';
|
|
6
6
|
export * from './helper.js';
|
|
7
|
-
export declare class StockLoader
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export declare class StockLoader {
|
|
8
|
+
get isDebug(): boolean;
|
|
9
|
+
static init(isDebug?: boolean): Promise<void>;
|
|
10
|
+
loadFxHistory(): Promise<IFXs>;
|
|
11
|
+
loadFx(): Promise<IFx>;
|
|
12
|
+
loadPrices(market: EnMarket): Promise<IStock[]>;
|
|
10
13
|
loadHolidays(): Promise<IHoliday[]>;
|
|
14
|
+
loadHistory(fullNc: string): Promise<IStockPoint[]>;
|
|
15
|
+
private _splitByMarket;
|
|
16
|
+
loadDividends(ncs: string[]): Promise<{
|
|
17
|
+
ign: string[];
|
|
18
|
+
points: IDiv[];
|
|
19
|
+
divPool: Record<string, IDiv[]>;
|
|
20
|
+
}>;
|
|
11
21
|
}
|
package/types/sgx.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IHoliday, IStock, IStockPoint } from './stock.js';
|
|
2
|
+
export declare class SGXLoader {
|
|
3
|
+
private __fetch;
|
|
4
|
+
private __laodIbmCode;
|
|
5
|
+
loadDividend(nc: string): Promise<import("./stock.js").IDivBonInfo>;
|
|
6
|
+
loadHistory(nc: string, int?: string): Promise<IStockPoint[]>;
|
|
7
|
+
loadPrices(): Promise<IStock[]>;
|
|
8
|
+
loadHolidays(year?: number): Promise<IHoliday[]>;
|
|
9
|
+
}
|
package/types/stock.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { IHoliday } from './stock.js';
|
|
1
2
|
/**
|
|
2
3
|
* 生成美股休市 & 提前收盘日历(ICS格式)
|
|
3
4
|
* @param year 年份 如 2025
|
|
4
5
|
* @returns ics 文本,可直接保存为 .ics 文件
|
|
5
6
|
*/
|
|
6
|
-
export declare function generateUSHolidays(year
|
|
7
|
+
export declare function generateUSHolidays(year?: number): IHoliday[];
|
package/types/web-request.d.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { type Page } from 'playwright';
|
|
2
2
|
export default class WebRequest {
|
|
3
|
-
private static HKEX_TOKEN;
|
|
4
3
|
private static browser?;
|
|
5
4
|
private static context?;
|
|
6
|
-
private static
|
|
5
|
+
private static __idles;
|
|
7
6
|
private static __promise?;
|
|
8
|
-
|
|
7
|
+
private static __isDebug;
|
|
9
8
|
protected page?: Page;
|
|
10
9
|
private static __getInstance;
|
|
11
|
-
get isDebug(): boolean;
|
|
10
|
+
static get isDebug(): boolean;
|
|
12
11
|
static init(isDebug?: boolean): Promise<void>;
|
|
13
12
|
constructor();
|
|
14
|
-
|
|
13
|
+
protected _mount(addr?: string): Promise<void>;
|
|
15
14
|
private static destroy;
|
|
16
15
|
destroy(): Promise<void>;
|
|
16
|
+
protected fetch(url: string): Promise<any>;
|
|
17
|
+
protected fetch(url: string, params?: Record<string, any>, headers?: Record<string, any>): Promise<any>;
|
|
17
18
|
protected fetch(url: string, method?: string, params?: Record<string, any>, headers?: Record<string, any>): Promise<any>;
|
|
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>;
|
|
21
20
|
protected read<T>(key: string): Promise<T>;
|
|
22
|
-
protected _loadHkexToken(): Promise<string>;
|
|
23
21
|
}
|
|
@@ -43,8 +43,11 @@ function toUTCs(d) {
|
|
|
43
43
|
* @param year 年份 如 2025
|
|
44
44
|
* @returns ics 文本,可直接保存为 .ics 文件
|
|
45
45
|
*/
|
|
46
|
-
export function generateUSHolidays(year) {
|
|
46
|
+
export function generateUSHolidays(year = 0) {
|
|
47
47
|
const events = [];
|
|
48
|
+
if (!year) {
|
|
49
|
+
year = new Date().getUTCFullYear();
|
|
50
|
+
}
|
|
48
51
|
// 1. 元旦(周六不补,周日补周一)
|
|
49
52
|
const newYear = new Date(year, 0, 1);
|
|
50
53
|
if (6 !== newYear.getDay()) {
|
package/web-request.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
|
-
import { A_DAY_MS } from './close-time.js';
|
|
3
2
|
export default class WebRequest {
|
|
4
|
-
static HKEX_TOKEN = { token: '', t: 0 };
|
|
5
3
|
static browser;
|
|
6
4
|
static context;
|
|
7
|
-
static
|
|
5
|
+
static __idles = [];
|
|
8
6
|
static __promise;
|
|
9
|
-
static
|
|
7
|
+
static __isDebug = false;
|
|
10
8
|
page;
|
|
11
|
-
static async __getInstance(
|
|
9
|
+
static async __getInstance() {
|
|
12
10
|
if (WebRequest.context)
|
|
13
11
|
return;
|
|
14
12
|
// 1. 启动 Chromium,禁用 web security(解除 CORS)
|
|
15
13
|
WebRequest.browser = await chromium.launch({
|
|
16
14
|
executablePath: '/opt/microsoft/msedge/msedge',
|
|
17
|
-
headless: !
|
|
15
|
+
headless: !WebRequest.isDebug,
|
|
18
16
|
channel: 'msedge',
|
|
19
17
|
args: [
|
|
20
18
|
'--disable-web-security',
|
|
@@ -24,31 +22,44 @@ export default class WebRequest {
|
|
|
24
22
|
]
|
|
25
23
|
});
|
|
26
24
|
WebRequest.context = await WebRequest.browser.newContext({});
|
|
25
|
+
const [foo, ...list] = WebRequest.context.pages();
|
|
26
|
+
list.length && list.forEach(page => page.close());
|
|
27
|
+
await foo.goto('about:blank');
|
|
28
|
+
WebRequest.__idles = [foo];
|
|
27
29
|
}
|
|
28
|
-
get isDebug() {
|
|
29
|
-
return WebRequest.
|
|
30
|
+
static get isDebug() {
|
|
31
|
+
return WebRequest.__isDebug;
|
|
30
32
|
}
|
|
31
33
|
static async init(isDebug = false) {
|
|
32
|
-
WebRequest.
|
|
34
|
+
WebRequest.__isDebug = isDebug;
|
|
33
35
|
if (!WebRequest.__promise)
|
|
34
|
-
WebRequest.__promise = WebRequest.__getInstance(
|
|
36
|
+
WebRequest.__promise = WebRequest.__getInstance();
|
|
35
37
|
}
|
|
36
38
|
constructor() { }
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
+
async _mount(addr = 'about:blank') {
|
|
40
|
+
if (!WebRequest.__promise)
|
|
41
|
+
WebRequest.__promise = WebRequest.__getInstance();
|
|
42
|
+
try {
|
|
43
|
+
await WebRequest.__promise;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
WebRequest.__promise = undefined;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
if (WebRequest.__idles.length) {
|
|
50
|
+
this.page = WebRequest.__idles.pop();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
39
53
|
this.page = await WebRequest.context.newPage();
|
|
40
|
-
|
|
41
|
-
await this.page.goto('about:blank');
|
|
54
|
+
await this.page.goto(addr);
|
|
42
55
|
}
|
|
43
56
|
static async destroy() {
|
|
44
|
-
WebRequest.
|
|
45
|
-
if (0 < WebRequest.pageCnt)
|
|
57
|
+
if (WebRequest.context?.pages().length)
|
|
46
58
|
return;
|
|
47
59
|
await WebRequest.context?.close();
|
|
48
60
|
await WebRequest.browser?.close();
|
|
49
61
|
WebRequest.context = undefined;
|
|
50
62
|
WebRequest.browser = undefined;
|
|
51
|
-
WebRequest.pageCnt = 0;
|
|
52
63
|
WebRequest.__promise = undefined;
|
|
53
64
|
}
|
|
54
65
|
async destroy() {
|
|
@@ -58,7 +69,12 @@ export default class WebRequest {
|
|
|
58
69
|
this.page = undefined;
|
|
59
70
|
return WebRequest.destroy();
|
|
60
71
|
}
|
|
61
|
-
fetch(url,
|
|
72
|
+
fetch(url, ...args) {
|
|
73
|
+
let method, params, headers;
|
|
74
|
+
[method, params, headers] = args;
|
|
75
|
+
if ('string' !== typeof method) {
|
|
76
|
+
[method, params, headers] = ['GET', method, params];
|
|
77
|
+
}
|
|
62
78
|
method = method.toUpperCase();
|
|
63
79
|
let data = '';
|
|
64
80
|
if (params) {
|
|
@@ -96,31 +112,13 @@ export default class WebRequest {
|
|
|
96
112
|
return resp.arrayBuffer();
|
|
97
113
|
}, { method, url, headers, data });
|
|
98
114
|
}
|
|
99
|
-
get(url, params, headers) {
|
|
100
|
-
return this.fetch(url, 'GET', params, headers);
|
|
101
|
-
}
|
|
102
115
|
goto(pageUrl) {
|
|
103
|
-
|
|
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 });
|
|
116
|
+
// 'load' 事件在行情页因持续的网络请求常常无法触发,导致超时
|
|
117
|
+
return this.page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
|
|
110
118
|
}
|
|
111
119
|
read(key) {
|
|
112
120
|
return this.page.evaluate(async (conf) => {
|
|
113
121
|
return window[conf.key];
|
|
114
122
|
}, { key });
|
|
115
123
|
}
|
|
116
|
-
async _loadHkexToken() {
|
|
117
|
-
const expiry = Date.now() - A_DAY_MS;
|
|
118
|
-
if (WebRequest.HKEX_TOKEN?.t > expiry)
|
|
119
|
-
return WebRequest.HKEX_TOKEN.token;
|
|
120
|
-
const doc = await this.get('https://www.hkex.com.hk/Market-Data/Securities-Prices/Equities?sc_lang=zh-HK');
|
|
121
|
-
const token = doc.split('//return "Base64-AES-Encrypted-Token";')[1].split('";')[0].split('"')[1];
|
|
122
|
-
const decodedToken = decodeURIComponent(token);
|
|
123
|
-
WebRequest.HKEX_TOKEN = { token: decodedToken, t: Date.now() };
|
|
124
|
-
return decodedToken;
|
|
125
|
-
}
|
|
126
124
|
}
|
package/yahoo-finance-loader.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { getMarketCloseTime } from './close-time.js';
|
|
2
|
-
import { splitFullNc, ttm } from './helper.js';
|
|
2
|
+
import { splitFullNc, toDivPoint, ttm } from './helper.js';
|
|
3
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
4
|
function toYahooSymbol(nc, market) {
|
|
16
5
|
switch (market) {
|
|
17
6
|
case EnMarket.USA:
|
|
@@ -35,23 +24,22 @@ export default class YahooFinanceLoader {
|
|
|
35
24
|
async getJson(url, params) {
|
|
36
25
|
const uri = new URL(url);
|
|
37
26
|
if (params) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}, {})).toString();
|
|
27
|
+
const sp = uri.searchParams;
|
|
28
|
+
Object.entries(params).forEach(([k, v]) => sp.append(k, v.toString()));
|
|
29
|
+
uri.search = sp.toString();
|
|
42
30
|
}
|
|
43
31
|
const resp = await fetch(uri.toString());
|
|
44
32
|
if (!resp.ok)
|
|
45
33
|
throw new Error(`HTTP ${resp.status}`);
|
|
46
|
-
|
|
34
|
+
const { chart, quoteResponse } = await resp.json();
|
|
35
|
+
return { chart, quote: quoteResponse?.result };
|
|
47
36
|
}
|
|
48
37
|
async getChart(symbol, params) {
|
|
49
38
|
const resp = await this.getJson(`https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}`, params);
|
|
50
|
-
const error = resp.chart
|
|
51
|
-
if (error?.description)
|
|
39
|
+
const { result, error } = resp.chart || {};
|
|
40
|
+
if (error?.description)
|
|
52
41
|
throw new Error(error.description);
|
|
53
|
-
|
|
54
|
-
const data = resp.chart?.result?.[0];
|
|
42
|
+
const data = result?.[0];
|
|
55
43
|
if (!data)
|
|
56
44
|
throw new Error(`empty chart data for ${symbol}`);
|
|
57
45
|
return data;
|
|
@@ -124,13 +112,11 @@ export default class YahooFinanceLoader {
|
|
|
124
112
|
return { result, remain };
|
|
125
113
|
}
|
|
126
114
|
async loadFx() {
|
|
127
|
-
const
|
|
128
|
-
const rows = resp.quoteResponse?.result || [];
|
|
115
|
+
const { quote: rows = [] } = await this.getJson('https://query1.finance.yahoo.com/v7/finance/quote', { symbols: 'USDHKD=X,USDSGD=X,USDCNY=X' });
|
|
129
116
|
const values = rows.reduce((pre, item) => {
|
|
130
|
-
const sym =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
pre[nc] = item.regularMarketPrice;
|
|
117
|
+
const { symbol: sym = '', regularMarketPrice } = item;
|
|
118
|
+
if (regularMarketPrice)
|
|
119
|
+
pre[sym.slice(3, 6)] = regularMarketPrice;
|
|
134
120
|
return pre;
|
|
135
121
|
}, {});
|
|
136
122
|
return {
|