Skip to content

Commit 79230e0

Browse files
committed
merge(v0.0.3): merge v0.0.3 to main branch
2 parents d265d3e + 0fc8b1c commit 79230e0

9 files changed

Lines changed: 260 additions & 96 deletions

File tree

dist/index.cjs

Lines changed: 45 additions & 46 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fxrate",
3-
"version": "0.0.2-1",
3+
"version": "0.0.3",
44
"license": "SEE LICENSE IN LICENSE",
55
"author": "Bo Xu <i@186526.xyz> (https://186526.xyz/)",
66
"dependencies": {
@@ -10,7 +10,7 @@
1010
"es-main": "^1.3.0",
1111
"fast-xml-parser": "^4.4.1",
1212
"feed": "^4.2.2",
13-
"handlers.js": "0.1.2-1",
13+
"handlers.js": "0.1.3-3",
1414
"handlers.js-jsonrpc": "0.0.3",
1515
"lru-cache": "^10.2.0",
1616
"mathjs": "^12.3.2",

src/FXGetter/ccb.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import { XMLParser } from 'fast-xml-parser';
22
import { FXRate, currency } from 'src/types';
33
import axios from 'axios';
44

5+
import crypto from 'crypto';
6+
import https from 'https';
7+
8+
/**
9+
* Handle this problem with Node 18
10+
* write EPROTO B8150000:error:0A000152:SSL routines:final_renegotiate:unsafe legacy renegotiation disabled
11+
* **/
12+
const allowLegacyRenegotiationforNodeJsOptions = {
13+
httpsAgent: new https.Agent({
14+
// allow sb CCB to use legacy renegotiation
15+
// 💩 CCB
16+
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
17+
}),
18+
};
19+
520
const parser = new XMLParser();
621

722
const currencyMap = {
@@ -25,12 +40,22 @@ const currencyMap = {
2540
'458': { name: 'MYR' as currency.MYR },
2641
'643': { name: 'RUB' as currency.RUB },
2742
'398': { name: 'KZT' as currency.KZT },
43+
'784': { name: 'AED' as currency.AED },
44+
'682': { name: 'SAR' as currency.SAR },
45+
'348': { name: 'HUF' as currency.HUF },
46+
'484': { name: 'MXN' as currency.MXN },
47+
'985': { name: 'PLN' as currency.PLN },
48+
'949': { name: 'TRY' as currency.TRY },
49+
'203': { name: 'CZK' as currency.CZK },
50+
'376': { name: 'ILS' as currency.ILS },
51+
'496': { name: 'MNT' as currency.MNT },
2852
};
2953

3054
const getCCBFXRates = async (): Promise<FXRate[]> => {
3155
const req = await axios.get(
32-
'http://www.ccb.com/cn/home/news/jshckpj_new.xml',
56+
'https://www.ccb.com/cn/home/news/jshckpj_new.xml',
3357
{
58+
...allowLegacyRenegotiationforNodeJsOptions,
3459
headers: {
3560
'User-Agent':
3661
process.env['HEADER_USER_AGENT'] ?? 'fxrate axios/latest',
@@ -40,7 +65,15 @@ const getCCBFXRates = async (): Promise<FXRate[]> => {
4065
const settlements = parser.parse(req.data)['ReferencePriceSettlements'][
4166
'ReferencePriceSettlement'
4267
];
68+
4369
const result = settlements.map((data: any) => {
70+
if (!(data['Ofrd_Ccy_CcyCd'] in currencyMap)) {
71+
console.log(
72+
`[${new Date().toUTCString()}] [CCB] Unsupported currency code ${data['Ofrd_Ccy_CcyCd']}, skipped.`,
73+
);
74+
return null;
75+
}
76+
4477
return {
4578
currency: {
4679
from: currencyMap[data['Ofrd_Ccy_CcyCd']].name,

src/FXGetter/hsbc.hk.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,48 @@ const getHSBCHKFXRates = async (): Promise<FXRate[]> => {
1515

1616
const data = req.data.detailRates;
1717

18-
const answer: FXRate[] = data.map((k) => {
19-
const answer: FXRate = {
20-
currency: {
21-
from: k.ccy as currency.unknown,
22-
to: 'HKD' as currency.HKD,
23-
},
24-
rate: {
25-
buy: {},
26-
sell: {},
27-
},
28-
updated: new Date(k.lastUpdateDate),
29-
unit: 1,
30-
};
31-
32-
if (k.ttBuyRt) answer.rate.buy.remit = parseFloat(k.ttBuyRt);
33-
if (k.bankBuyRt) answer.rate.buy.cash = parseFloat(k.bankBuyRt);
34-
if (k.ttSelRt) answer.rate.sell.remit = parseFloat(k.ttSelRt);
35-
if (k.bankSellRt) answer.rate.sell.cash = parseFloat(k.bankSellRt);
36-
37-
return answer;
38-
});
39-
40-
answer.push(
41-
((answer) => {
42-
const tmp = answer.find((k) => k.currency.from === 'CNY');
43-
tmp.currency.from = 'CNH' as currency.CNH;
44-
return tmp;
45-
})(answer),
46-
);
47-
48-
return answer;
18+
const answers: FXRate[] = data
19+
.map((k) => {
20+
const answer: FXRate = {
21+
currency: {
22+
from: k.ccy as currency.unknown,
23+
to: 'HKD' as currency.HKD,
24+
},
25+
rate: {
26+
buy: {},
27+
sell: {},
28+
},
29+
updated: new Date(k.lastUpdateDate),
30+
unit: 1,
31+
};
32+
33+
if (k.ttBuyRt) answer.rate.buy.remit = parseFloat(k.ttBuyRt);
34+
if (k.bankBuyRt) answer.rate.buy.cash = parseFloat(k.bankBuyRt);
35+
if (k.ttSelRt) answer.rate.sell.remit = parseFloat(k.ttSelRt);
36+
if (k.bankSellRt) answer.rate.sell.cash = parseFloat(k.bankSellRt);
37+
38+
if (answer.currency.from == 'CNY') {
39+
const CNHAnswer: FXRate = {
40+
...answer,
41+
currency: {
42+
...answer.currency,
43+
from: 'CNH' as currency.CNH,
44+
},
45+
rate: {
46+
buy: { ...answer.rate.buy },
47+
sell: { ...answer.rate.sell },
48+
},
49+
};
50+
51+
console.log(answer, CNHAnswer);
52+
53+
return [answer, CNHAnswer];
54+
} else return answer;
55+
})
56+
.flat()
57+
.sort();
58+
59+
return answers;
4960
};
5061

5162
export default getHSBCHKFXRates;

src/FXGetter/spdb.d.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Represents exchange rate and pricing information returned from the SPDB remote service.
3+
*
4+
* This interface models a flat (string-valued) payload typically obtained from a SOAP/HTTP response.
5+
* All fields are represented as strings in the original payload; numeric values may be formatted with
6+
* fixed decimals and should be parsed to numbers by consumers when needed.
7+
*
8+
* @remarks
9+
* Keep in mind:
10+
* - Timestamp fields use the source format (e.g. "YYYY.MM.DD HH:mm:ss") and may require parsing.
11+
* - Price and rate fields are often scaled (e.g. "100.000000") and may represent per-unit or per-100 units,
12+
* depending on ExgRtUnt.
13+
*
14+
* @property RET - Raw response fragment (often contains the beginning of a SOAP/XML envelope).
15+
* @property ReturnCode - Service return/response code (empty string when no code is present).
16+
* @property UnchSellPrc - "Unchanged" sell price (string-formatted decimal).
17+
* @property docid - Document identifier for the record.
18+
* @property AnlSetlExgRt - Analytical settlement exchange rate (string-formatted decimal).
19+
* @property SellPrc - Sell price (string-formatted decimal).
20+
* @property CurrencyId - Currency identifier/code (e.g. "01").
21+
* @property CurrencyName - Human readable currency name like '美元 USD'.
22+
* @property CashBuyPrc - Cash buy price (string-formatted decimal).
23+
* @property CREATE_DATE - Creation date/time as provided by the source (e.g. "2025.10.20 22:30:16").
24+
* @property BuyPrc - Buy price (string-formatted decimal).
25+
* @property MdlPrc - Middle/median price or model price (string-formatted decimal).
26+
* @property CashSellPrc - Cash sell price (string-formatted decimal).
27+
* @property UnchBuyPrc - "Unchanged" buy price (string-formatted decimal).
28+
* @property USDCnvrPrc - USD conversion price (string-formatted decimal; may be used to derive cross-rates).
29+
* @property ctime - Processing or cache time indicator (source-specific meaning; often numeric string).
30+
* @property state - State or status code for the record (source-specific string).
31+
* @property ExgRtUnt - Exchange rate unit (e.g. "100" means rates are per 100 units).
32+
* @property EurSetlPrc - EUR settlement price (string-formatted decimal).
33+
*
34+
*/
35+
36+
export interface SPDBFXReqInfo {
37+
RET: string;
38+
ReturnCode: string;
39+
UnchSellPrc: string;
40+
docid: string;
41+
AnlSetlExgRt: string;
42+
SellPrc: string;
43+
CurrencyId: string;
44+
CurrencyName: string;
45+
CashBuyPrc: string;
46+
CREATE_DATE: string;
47+
BuyPrc: string;
48+
MdlPrc: string;
49+
CashSellPrc: string;
50+
UnchBuyPrc: string;
51+
USDCnvrPrc: string;
52+
ctime: string;
53+
state: string;
54+
ExgRtUnt: string;
55+
EurSetlPrc: string;
56+
}

src/FXGetter/spdb.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,84 @@ import cheerio from 'cheerio';
44

55
import { currency, FXRate } from 'src/types';
66

7+
import crypto from 'crypto';
8+
import https from 'https';
9+
import { SPDBFXReqInfo } from './spdb.d';
10+
11+
/**
12+
* Handle this problem with Node 18
13+
* write EPROTO B8150000:error:0A000152:SSL routines:final_renegotiate:unsafe legacy renegotiation disabled
14+
* **/
15+
const allowLegacyRenegotiationforNodeJsOptions = {
16+
httpsAgent: new https.Agent({
17+
// allow sb SPDB to use legacy renegotiation
18+
// 💩 SPDB
19+
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
20+
}),
21+
};
22+
723
const getSPDBFXRates = async (): Promise<FXRate[]> => {
8-
const req = await axios.get(
9-
'https://www.spdb.com.cn/was5/web/search?channelid=256931',
24+
const req = await axios.post(
25+
'https://www.spdb.com.cn/api/search',
1026
{
27+
metadata: 'NAME|ASK|BID|CODE|CREATE_DATE',
28+
size: 100,
29+
chlid: 1061,
30+
searchword: '',
31+
},
32+
{
33+
...allowLegacyRenegotiationforNodeJsOptions,
1134
headers: {
1235
'User-Agent':
1336
process.env['HEADER_USER_AGENT'] ?? 'fxrate axios/latest',
1437
},
1538
},
1639
);
1740

41+
const data: SPDBFXReqInfo[] = req.data.data.content;
42+
43+
return data
44+
.map((d) => {
45+
if (!d.CurrencyName) return null; // means that currency not visible globally (secret for SPDB)
46+
47+
const fromCurrency = d.CurrencyName.split(' ')[1] as currency;
48+
49+
return {
50+
currency: {
51+
from: fromCurrency,
52+
to: 'CNY' as currency.CNY,
53+
},
54+
55+
rate: {
56+
buy: {
57+
cash: parseFloat(d.CashBuyPrc),
58+
remit: parseFloat(d.BuyPrc),
59+
},
60+
sell: {
61+
cash: parseFloat(d.CashSellPrc),
62+
remit: parseFloat(d.SellPrc),
63+
},
64+
middle: parseFloat(d.MdlPrc),
65+
},
66+
67+
updated: new Date(d['CREATE_DATE'] + ' UTC+8'),
68+
unit: parseInt(d.ExgRtUnt),
69+
} as FXRate;
70+
})
71+
.sort();
72+
};
73+
74+
const getSPDBFXRatesByOldHTML = async (): Promise<FXRate[]> => {
75+
const req = await axios.get('https://www.spdb.com.cn/wh_pj/index.shtml', {
76+
...allowLegacyRenegotiationforNodeJsOptions,
77+
headers: {
78+
'User-Agent':
79+
process.env['HEADER_USER_AGENT'] ?? 'fxrate axios/latest',
80+
},
81+
});
82+
83+
console.log(req.data);
84+
1885
const $ = cheerio.load(req.data);
1986

2087
const updatedTime = new Date($('.fine_title > p').text() + ' UTC+8');
@@ -54,3 +121,5 @@ const getSPDBFXRates = async (): Promise<FXRate[]> => {
54121
};
55122

56123
export default getSPDBFXRates;
124+
125+
export { getSPDBFXRatesByOldHTML };

src/fxm/fxManager.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export default class fxManager {
9393
}
9494

9595
public update(FXRate: FXRate): void {
96+
if (FXRate === null) return;
97+
9698
const { currency, unit } = FXRate;
9799
let { rate } = FXRate;
98100

@@ -101,13 +103,6 @@ export default class fxManager {
101103
if (from == ('RMB' as currency.RMB)) from = 'CNY' as currency.CNY;
102104
if (to == ('RMB' as currency.RMB)) to = 'CNY' as currency.CNY;
103105

104-
// if (from == ('CNH' as currency.CNH) || to == ('CNH' as currency.CNH)) {
105-
// const CNYFXrates = Object.assign({}, FXRate);
106-
// CNYFXrates.currency.from = CNYFXrates.currency.from == 'CNH' ? 'CNY' as currency.CNY : CNYFXrates.currency.from;
107-
// CNYFXrates.currency.to = CNYFXrates.currency.to == 'CNH' ? 'CNY' as currency.CNY : CNYFXrates.currency.to;
108-
// this.update(CNYFXrates);
109-
// }
110-
111106
if (this.fxRateList[from] && this.fxRateList[from][to]) {
112107
if (this.fxRateList[from][to].updated > FXRate.updated) return;
113108
}

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"incremental": true,
1414
"skipLibCheck": true,
1515
"moduleResolution": "node",
16+
"ignoreDeprecations": "6.0",
1617
"resolveJsonModule": true
1718
},
1819
"exclude": ["dist", "node_modules"],

yarn.lock

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2507,10 +2507,10 @@ handlers.js-jsonrpc@0.0.3:
25072507
resolved "https://registry.npmmirror.com/handlers.js-jsonrpc/-/handlers.js-jsonrpc-0.0.3.tgz#190bd5afb95444e226cd5370c7eda601b60aae95"
25082508
integrity sha512-uzuAh5hT/RZh8dGMF9byW0427Rb4+ndeqgO6BygE9TCcSADVLbs2c14SxJMFaqr/n2LzNsQWR17SUQ6wQpSwnw==
25092509

2510-
handlers.js@0.1.2-1:
2511-
version "0.1.2-1"
2512-
resolved "https://registry.npmjs.org/handlers.js/-/handlers.js-0.1.2-1.tgz#96c262b6539da28d0547ed8b35542a663e4f2552"
2513-
integrity sha512-7oSxE85Nd+xbQdp5YNhFCGtxLlQf5+Wc7/mixEZal21ImLzxLf6CTDawWCr4cYZLD9zVs2HHIeFLQKRUWb2qHg==
2510+
handlers.js@0.1.3-3:
2511+
version "0.1.3-3"
2512+
resolved "https://registry.yarnpkg.com/handlers.js/-/handlers.js-0.1.3-3.tgz#cec29f575e71360a6aa223b97376f628896055f8"
2513+
integrity sha512-8DBkeDACBBrn4PW+LrryEX+rpLyrgbTiePcBeQsisdybyQzmll6JZnqmSJYpieb3FK71HdoinOhhSVKbNVE7Ag==
25142514
dependencies:
25152515
path-to-regexp "6"
25162516

@@ -3577,9 +3577,9 @@ path-parse@^1.0.7:
35773577
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
35783578

35793579
path-to-regexp@6:
3580-
version "6.2.2"
3581-
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36"
3582-
integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==
3580+
version "6.3.0"
3581+
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
3582+
integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
35833583

35843584
path-type@^4.0.0:
35853585
version "4.0.0"

0 commit comments

Comments
 (0)