@v587d/capital-generation 2.8.2 → 2.9.0
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/CHANGELOG.md +87 -1
- package/README.md +41 -20
- package/chart-ui/README.md +1 -1
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +3 -8
- package/lib/chart/artifact-ref.js +3 -8
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +40 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +6 -29
- package/lib/chart/tool.js +9 -23
- package/lib/chart-render-entry.d.ts +9 -0
- package/lib/chart-render-entry.js +17 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +40 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -31
- package/lib/sources/ftshare-rest.js +1194 -225
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +39 -64
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +67 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +526 -337
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
|
@@ -6,22 +6,16 @@ export interface SecurityCode {
|
|
|
6
6
|
tencentSymbol: string;
|
|
7
7
|
isIndex: boolean;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Parse an A-share/ETF/index code without dropping exchange information.
|
|
11
|
-
* Bare 000xxx codes are rejected because they are ambiguous (e.g. 000001).
|
|
12
|
-
*/
|
|
9
|
+
/** Parse an A-share/ETF/index code; bare 000xxx is rejected as ambiguous (000001). */
|
|
13
10
|
export declare function parseSecurityCode(value: unknown, name?: string): SecurityCode;
|
|
14
11
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* sseinfo 只覆盖沪市,越界返回 0 条并带 note 是真事实,不能在这一层把它拒掉。
|
|
20
|
-
* 所以裸 `000001`(平安银行)在这里合法,`SH600519` / `600519.SH` 也合法(东财研报库
|
|
21
|
-
* 实测只认纯数字,带前缀返回 0 条,因此归一化属于工具职责而不是模型的记性)。
|
|
12
|
+
* Search-side code normalization: reduce a spelling to 6 digits.
|
|
13
|
+
* Unlike `parseSecurityCode`, bare `000001` is legal: named sources cover one exchange
|
|
14
|
+
* (cninfo SZ-only, sseinfo SH-only) and must be allowed to answer "no rows" themselves.
|
|
15
|
+
* Eastmoney research reports accept digits only; prefixed input returns 0 rows.
|
|
22
16
|
*/
|
|
23
17
|
export declare function bareAshareDigits(value: unknown, name?: string): string;
|
|
24
|
-
/**
|
|
18
|
+
/** Dedupe after normalization, preserving first-seen order. */
|
|
25
19
|
export declare function normalizeSecurityCodes(value: unknown, name?: string, maximum?: number): SecurityCode[];
|
|
26
20
|
export declare function requireTencentSecurity(code: SecurityCode, name?: string): SecurityCode;
|
|
27
21
|
export declare function isLikelyIndex(code: SecurityCode): boolean;
|
|
@@ -17,10 +17,7 @@ function explicitMarket(prefix, suffix) {
|
|
|
17
17
|
function isMarket(value) {
|
|
18
18
|
return value === 'SH' || value === 'SZ' || value === 'BJ';
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Parse an A-share/ETF/index code without dropping exchange information.
|
|
22
|
-
* Bare 000xxx codes are rejected because they are ambiguous (e.g. 000001).
|
|
23
|
-
*/
|
|
20
|
+
/** Parse an A-share/ETF/index code; bare 000xxx is rejected as ambiguous (000001). */
|
|
24
21
|
export function parseSecurityCode(value, name = 'code') {
|
|
25
22
|
if (typeof value !== 'string' || value.trim().length === 0)
|
|
26
23
|
throw new Error(`parameter ${name} must be a security code`);
|
|
@@ -54,13 +51,10 @@ export function parseSecurityCode(value, name = 'code') {
|
|
|
54
51
|
};
|
|
55
52
|
}
|
|
56
53
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* sseinfo 只覆盖沪市,越界返回 0 条并带 note 是真事实,不能在这一层把它拒掉。
|
|
62
|
-
* 所以裸 `000001`(平安银行)在这里合法,`SH600519` / `600519.SH` 也合法(东财研报库
|
|
63
|
-
* 实测只认纯数字,带前缀返回 0 条,因此归一化属于工具职责而不是模型的记性)。
|
|
54
|
+
* Search-side code normalization: reduce a spelling to 6 digits.
|
|
55
|
+
* Unlike `parseSecurityCode`, bare `000001` is legal: named sources cover one exchange
|
|
56
|
+
* (cninfo SZ-only, sseinfo SH-only) and must be allowed to answer "no rows" themselves.
|
|
57
|
+
* Eastmoney research reports accept digits only; prefixed input returns 0 rows.
|
|
64
58
|
*/
|
|
65
59
|
export function bareAshareDigits(value, name = 'code') {
|
|
66
60
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
@@ -72,7 +66,7 @@ export function bareAshareDigits(value, name = 'code') {
|
|
|
72
66
|
}
|
|
73
67
|
return match[2] ?? match[3];
|
|
74
68
|
}
|
|
75
|
-
/**
|
|
69
|
+
/** Dedupe after normalization, preserving first-seen order. */
|
|
76
70
|
export function normalizeSecurityCodes(value, name = 'codes', maximum = 50) {
|
|
77
71
|
const rawValues = Array.isArray(value)
|
|
78
72
|
? value
|
|
@@ -5,10 +5,7 @@ export interface EastmoneySecurityIdentity {
|
|
|
5
5
|
eastmoney_code: string;
|
|
6
6
|
eastmoney_market: EastmoneyMarket;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Resolve Eastmoney's provider identifiers without guessing a market from digits.
|
|
10
|
-
* `market` is mandatory for a bare SECURITY_CODE; `secid` carries its own market.
|
|
11
|
-
*/
|
|
8
|
+
/** Never guess a market from digits: `market` is mandatory for bare SECURITY_CODE; secid carries its own. */
|
|
12
9
|
export declare function normalizeEastmoneySecurityIdentity(input: {
|
|
13
10
|
secucode?: unknown;
|
|
14
11
|
security_code?: unknown;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { parseSecurityCode } from './security-code.js';
|
|
2
2
|
/**
|
|
3
|
-
* secid
|
|
4
|
-
* `
|
|
5
|
-
* 因此 `0.` 的 BJ/SZ 由数字前缀区分(4/8/92 为北交所),与 `normalizeEastmoneyAshareIdentity`
|
|
6
|
-
* 共用 `marketFromAshareDigits` 的同一份规则(§9.7:一条知识一个实现)。
|
|
3
|
+
* Eastmoney secid market prefix: `1.` = SH; `0.` = SZ and BJ combined (`2.*` returns rc=100).
|
|
4
|
+
* For `0.` the BJ/SZ split comes from digit prefixes, sharing `marketFromAshareDigits`.
|
|
7
5
|
*/
|
|
8
6
|
const SECID_MARKET_BY_PREFIX = { '1': 'SH', '0': 'SZ_OR_BJ' };
|
|
9
7
|
function marketFromAshareDigits(digits) {
|
|
@@ -38,10 +36,7 @@ function code(value, name) {
|
|
|
38
36
|
return raw;
|
|
39
37
|
throw new Error(`${name} must be a six-digit code, SECUCODE, or market-prefixed code`);
|
|
40
38
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Resolve Eastmoney's provider identifiers without guessing a market from digits.
|
|
43
|
-
* `market` is mandatory for a bare SECURITY_CODE; `secid` carries its own market.
|
|
44
|
-
*/
|
|
39
|
+
/** Never guess a market from digits: `market` is mandatory for bare SECURITY_CODE; secid carries its own. */
|
|
45
40
|
export function normalizeEastmoneySecurityIdentity(input) {
|
|
46
41
|
const explicit = input.secucode ?? input.security_code;
|
|
47
42
|
if (explicit !== undefined) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildDataKey } from '../data-collector/hub.js';
|
|
2
2
|
import { getDataTimeContract } from '../time/tools.js';
|
|
3
3
|
import { isLikelyIndex, normalizeSecurityCodes, parseSecurityCode, requireTencentSecurity } from './security-code.js';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Shared core holds HK/US snapshots, smartbox and the one throttle chain; this file only defines
|
|
5
|
+
// capabilities and K-line/tick orchestration. Two throttles = no throttle.
|
|
6
6
|
import { QUOTE_URL, fetchOffshoreIndexQuotes, fetchOffshoreQuotes, isRecord, normalizeHkCode, normalizeOffshoreIndexCode, normalizeUsCode, numberOrNull, requiredNumber, resolveUsKlineSymbol, sourceError, tencentGetText as getText, } from './tencent-public-core.js';
|
|
7
7
|
const TICK_URL = 'https://stock.gtimg.cn/data/index.php';
|
|
8
8
|
const KLINE_HOSTS = [
|
|
@@ -262,7 +262,8 @@ async function callKlineHosts(transport, signal) {
|
|
|
262
262
|
hostDownUntil.set(host, Date.now() + HOST_COOLDOWN_MS);
|
|
263
263
|
}
|
|
264
264
|
catch (error) {
|
|
265
|
-
//
|
|
265
|
+
// Cancellation and param failures rethrow before blacklist/rotation; see
|
|
266
|
+
// `docs/dev/web-retriever.md` §4.2.
|
|
266
267
|
if (error instanceof Error && error.name === 'AbortError')
|
|
267
268
|
throw error;
|
|
268
269
|
if (error.code === 'tencent_invalid_param')
|
|
@@ -463,13 +464,6 @@ async function executeTicks(params, signal) {
|
|
|
463
464
|
schema: ticksOutput,
|
|
464
465
|
};
|
|
465
466
|
}
|
|
466
|
-
/**
|
|
467
|
-
* 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
|
|
468
|
-
* 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
|
|
469
|
-
* 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
|
|
470
|
-
* 所以两张表各自映射(都在 `tencent-public-core.ts`,host 半边与这里共用),
|
|
471
|
-
* 并且只收能用算术或量级证明的列(§10.2:口径猜错就是话说错)。
|
|
472
|
-
*/
|
|
473
467
|
function normalizeOffshoreCodes(params, name, perCode) {
|
|
474
468
|
if (!isRecord(params))
|
|
475
469
|
throw new Error('params must be an object');
|
|
@@ -487,12 +481,10 @@ function normalizeOffshoreCodes(params, name, perCode) {
|
|
|
487
481
|
return { codes: symbols };
|
|
488
482
|
}
|
|
489
483
|
async function executeOffshoreQuote(params, signal, market) {
|
|
490
|
-
// 取数编排(含美股"没回行就剥一段后缀重问")在内核里,与自选股面板共用一份。
|
|
491
484
|
const rows = await fetchOffshoreQuotes(market, params.codes, signal);
|
|
492
485
|
return { data: rows, schema: market === 'hk' ? hkQuoteOutput : usQuoteOutput };
|
|
493
486
|
}
|
|
494
487
|
async function executeOffshoreIndexQuote(params, signal, market) {
|
|
495
|
-
// 指数只收点位那一族可证明的列;写法来自检索,身份形状由内核核对(猜写法会静默拿到别的标的)。
|
|
496
488
|
const rows = await fetchOffshoreIndexQuotes(market, params.codes, signal);
|
|
497
489
|
return { data: rows, schema: market === 'hk' ? hkIndexQuoteOutput : usIndexQuoteOutput };
|
|
498
490
|
}
|
|
@@ -500,7 +492,6 @@ const offshoreKlinePaths = {
|
|
|
500
492
|
hk: { qfq: '/appstock/app/hkfqkline/get', hfq: '/appstock/app/hkfqkline/get', none: '/appstock/app/kline/kline' },
|
|
501
493
|
us: { qfq: '/appstock/app/usfqkline/get', none: '/appstock/app/kline/kline' },
|
|
502
494
|
};
|
|
503
|
-
/** 港美股 K 线入口:主机轮换全部走 `callKlineHosts`(一份实现),这里只补"序列在哪个 symbol 的哪个键下"。 */
|
|
504
495
|
async function callOffshoreKline(path, param, symbol, signal) {
|
|
505
496
|
const data = await callKlineHosts({
|
|
506
497
|
path,
|
|
@@ -520,14 +511,14 @@ async function callOffshoreKline(path, param, symbol, signal) {
|
|
|
520
511
|
throw sourceError(`Tencent K-line ${symbol} response has no series array`, 'tencent_invalid_response');
|
|
521
512
|
return node[key];
|
|
522
513
|
}
|
|
523
|
-
/**
|
|
514
|
+
/** Row: `[date, open, close, high, low, volume, …]`; field 6 is buyback/dividend text. */
|
|
524
515
|
function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust, market) {
|
|
525
516
|
const rows = [];
|
|
526
517
|
for (const item of items) {
|
|
527
518
|
if (!Array.isArray(item) || item.length < 6)
|
|
528
519
|
throw sourceError(`Tencent ${market} K-line ${tencentSymbol} row shape changed`, 'tencent_invalid_response');
|
|
529
520
|
const date = String(item[0]);
|
|
530
|
-
//
|
|
521
|
+
// Shape alone accepts 2026-13-99, so re-read through the real calendar.
|
|
531
522
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || (() => { const parsed = new Date(`${date}T00:00:00Z`); return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date; })()) {
|
|
532
523
|
throw sourceError(`Tencent ${market} K-line ${tencentSymbol} has unreadable date ${JSON.stringify(date)}`, 'tencent_invalid_response');
|
|
533
524
|
}
|
|
@@ -551,7 +542,7 @@ function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust,
|
|
|
551
542
|
volume,
|
|
552
543
|
};
|
|
553
544
|
if (market === 'hk') {
|
|
554
|
-
//
|
|
545
|
+
// Adjusted series has two extra columns; unadjusted has seven, so both are null.
|
|
555
546
|
row.turnover_pct = item.length > 7 ? numberOrNull(item[7]) : null;
|
|
556
547
|
row.amount_wan_hkd = item.length > 8 ? numberOrNull(item[8]) : null;
|
|
557
548
|
}
|
|
@@ -564,7 +555,7 @@ function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust,
|
|
|
564
555
|
function normalizeOffshoreKline(params, market) {
|
|
565
556
|
if (!isRecord(params))
|
|
566
557
|
throw new Error('params must be an object');
|
|
567
|
-
// start/end
|
|
558
|
+
// start/end are accepted only to reject them.
|
|
568
559
|
assertKnown(params, ['code', 'period', 'adjust', 'count', 'start', 'end']);
|
|
569
560
|
if (params.start !== undefined || params.end !== undefined)
|
|
570
561
|
throw new Error('start/end are not supported: 实测 hkfqkline 忽略日期区间(带 2024-01-01..2024-03-01 与 count=640 仍回 640 条、最早到 2021-07-30),只有 count 生效');
|
|
@@ -586,7 +577,7 @@ async function executeOffshoreKline(params, signal, market) {
|
|
|
586
577
|
const period = String(params.period);
|
|
587
578
|
const adjust = String(params.adjust);
|
|
588
579
|
const count = Number(params.count);
|
|
589
|
-
//
|
|
580
|
+
// HK is hk + 5 digits; US suffix must be resolved by the snapshot.
|
|
590
581
|
const tencentSymbol = market === 'hk' ? `hk${code}` : await resolveUsKlineSymbol(code, signal);
|
|
591
582
|
const canonical = market === 'hk' ? code : tencentSymbol.slice(2);
|
|
592
583
|
const path = offshoreKlinePaths[market][adjust];
|
|
@@ -654,12 +645,8 @@ const usQuoteOutput = {
|
|
|
654
645
|
},
|
|
655
646
|
};
|
|
656
647
|
/**
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* 为什么不收(2026-10-07 真报文):港股指数行的第 6 / 36 / 37 位两列相等且量级像万元,
|
|
660
|
-
* 美股指数行的第 37 位给了 2.1e14、第 38 / 44 / 45 / 62 / 63 位为空——口径推不出来,
|
|
661
|
-
* 声明一列我们没有把握的东西比少给一列更糟(docs/dev/tool-schema.md §10.2)。
|
|
662
|
-
* 为什么不收币种:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但指数是无量纲的数。
|
|
648
|
+
* Index output: point values only. Magnitude columns are unreadable (hkHSI 6/36/37 duplicate at
|
|
649
|
+
* 10k; usIXIC 37 is 2.1e14, 38/44/45/62/63 empty). No currency.
|
|
663
650
|
*/
|
|
664
651
|
const offshoreIndexQuoteProperties = (symbolLabel) => ({
|
|
665
652
|
code: { type: 'string' },
|
|
@@ -684,16 +671,15 @@ const usIndexQuoteOutput = {
|
|
|
684
671
|
type: 'array',
|
|
685
672
|
items: { type: 'object', properties: offshoreIndexQuoteProperties('美东'), additionalProperties: true },
|
|
686
673
|
};
|
|
687
|
-
/**
|
|
674
|
+
/** Columns `[date, open, close, high, low, volume]`, volume in shares. */
|
|
688
675
|
const offshoreKlineProperties = () => ({
|
|
689
676
|
code: { type: 'string' }, tencent_symbol: { type: 'string' }, adjust: { type: 'string' },
|
|
690
677
|
period: { type: 'string' }, date: { type: 'string' },
|
|
691
678
|
open: { type: 'number' }, high: { type: 'number' }, low: { type: 'number' }, close: { type: 'number' },
|
|
692
679
|
volume: { type: 'number', description: '成交量(股);不是 A 股 K 线那套"手"' },
|
|
693
680
|
});
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
// 所以美股这张表**没有** amount 字段——不声明一列恒为 null 的东西,那只会让人以为是缺数。
|
|
681
|
+
// HK adjusted series has two extra columns; its amount is in 10k HKD. US qfq/none return six
|
|
682
|
+
// columns, so the US table declares no amount field.
|
|
697
683
|
const hkKlineOutput = {
|
|
698
684
|
type: 'array',
|
|
699
685
|
items: {
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
export declare const QUOTE_URL = "https://qt.gtimg.cn/q=";
|
|
2
2
|
export declare const SMARTBOX_URL = "https://smartbox.gtimg.cn/s3/";
|
|
3
|
-
/**
|
|
4
|
-
* 腾讯出口的**唯一**节流链(进程级)。行情快照、分笔、K 线每一次换机、港美股快照、smartbox
|
|
5
|
-
* 检索全部过这一条链——它们打的是同一家,按 IP 风控(429 / 空 data / 静默限流)。
|
|
6
|
-
* 间隔取 120ms:与最早那版分笔翻页的节奏同量级。
|
|
7
|
-
*/
|
|
8
3
|
export declare const TENCENT_MIN_INTERVAL_MS = 120;
|
|
9
4
|
export declare const tencentThrottle: <T>(task: () => Promise<T>) => Promise<T>;
|
|
10
5
|
export type JsonRecord = Record<string, unknown>;
|
|
@@ -12,81 +7,35 @@ export declare function isRecord(value: unknown): value is JsonRecord;
|
|
|
12
7
|
export declare function numberOrNull(value: unknown): number | null;
|
|
13
8
|
export declare function requiredNumber(value: unknown, field: string): number;
|
|
14
9
|
export declare function sourceError(message: string, code?: string): Error;
|
|
15
|
-
/**
|
|
16
|
-
* 腾讯出口的取文本:取消不排队(队首那个请求挂住时,把这次取消塞进队列等于把它吞掉,
|
|
17
|
-
* `docs/dev/web-retriever.md` §4.2「取消原样抛出」),排队与最小间隔由节流链负责。
|
|
18
|
-
*/
|
|
10
|
+
/** Cancellation does not queue; see `docs/dev/web-retriever.md` §4.2. */
|
|
19
11
|
export declare function tencentGetText(url: string, signal: AbortSignal, encoding?: 'utf-8' | 'gbk', init?: RequestInit): Promise<string>;
|
|
20
|
-
/**
|
|
21
|
-
* 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
|
|
22
|
-
* 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
|
|
23
|
-
* 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
|
|
24
|
-
* 所以这两张表各自映射,并且只收能用算术或量级证明的列(docs/dev/tool-schema.md §10.2:口径猜错就是话说错)。
|
|
25
|
-
*/
|
|
12
|
+
/** Separate HK/US and A-share field tables: currency 75/35; turnover 59 (total shares) vs 38 (float). */
|
|
26
13
|
export declare const HK_QUOTE_FIELDS = 78;
|
|
27
14
|
export declare const US_QUOTE_FIELDS = 73;
|
|
28
|
-
/**
|
|
29
|
-
* 腾讯港美股时间戳是**交易所当地时间**。分隔符**按行漂移**:2026-10-05 那批同一份响应里,
|
|
30
|
-
* 一行给 `YYYY/MM/DD HH:mm:ss`、另一行给 `YYYY-MM-DD HH:mm:ss`——所以两种都吃,输出统一成横杠那一种。
|
|
31
|
-
* (写成占位符而不是真日期:这两个形状在源码里只差一个字符,肉眼读注释分不出来。)
|
|
32
|
-
*/
|
|
15
|
+
/** Exchange-local time; separator drifts per row (`/` vs `-`), normalize to hyphen. */
|
|
33
16
|
export declare function offshoreQuoteTime(value: unknown): string | null;
|
|
34
|
-
/** 逐行拆出 `v_<symbol>="…"`;symbol 就是请求里写出去的那串,所以请求与回执能一对一核对。 */
|
|
35
17
|
export declare function snapshotRows(text: string, requested: Set<string>): Array<{
|
|
36
18
|
symbol: string;
|
|
37
19
|
fields: string[];
|
|
38
20
|
}>;
|
|
39
|
-
/**
|
|
21
|
+
/** HK codes are 5 digits; `hk700`/`hk0700` are silently dropped upstream, so left-pad. */
|
|
40
22
|
export declare function normalizeHkCode(value: unknown, name: string): string;
|
|
41
|
-
/**
|
|
42
|
-
* 美股写法照原样收下(`AAPL` / `BRK.B` / `BABA.N` / `AAPL.OQ` 都是真实存在的说法),
|
|
43
|
-
* 消歧交给上游(见 `usSymbolVariants`):实测腾讯**快照只认不带后缀的写法**(`usBABA.N` 整行不回,
|
|
44
|
-
* `usBABA` 认),而 K 线只认**带正确后缀**的写法(`usAAPL` 回一列 2011 年的陌生序列、
|
|
45
|
-
* `usAAPL.NS` 静默 0 行)。点号既可能是级别码(BRK.B / BF.B)也可能是交易所后缀(.N / .OQ),
|
|
46
|
-
* 从字面上分不出来——所以不猜。
|
|
47
|
-
*/
|
|
23
|
+
/** Snapshot matches only bare US tickers (`usBABA.N` → no row); K-line needs the suffix (`usAAPL` → stale 2011 data). */
|
|
48
24
|
export declare function normalizeUsCode(value: unknown, name: string): string;
|
|
49
|
-
/** 依次尝试的写法:原样 → 每次剥掉最后一段点号,最多剥两段(BRK.B.N → BRK.B → BRK)。 */
|
|
50
25
|
export declare function usSymbolVariants(ticker: string): string[];
|
|
51
26
|
export declare function parseHkQuoteRows(text: string, requested: Set<string>): JsonRecord[];
|
|
52
27
|
export declare function parseUsQuoteRows(text: string, requested: Set<string>): Array<{
|
|
53
28
|
symbol: string;
|
|
54
29
|
row: JsonRecord;
|
|
55
30
|
}>;
|
|
56
|
-
/**
|
|
57
|
-
* **港美股指数快照**:只收点位那一族能证明的列。
|
|
58
|
-
*
|
|
59
|
-
* 为什么必须另开一张表(而不是复用个股那张):实测指数行的量纲列不可读——
|
|
60
|
-
* 港 `hkHSI` 的第 6 / 36 / 37 位两列相等且量级像万元,美 `usIXIC` 的第 37 位给了 2.1e14、
|
|
61
|
-
* 第 38 / 44 / 45 / 62 / 63 位是空。把这些列收进输出表就等于承诺一个我们证明不了的口径。
|
|
62
|
-
*
|
|
63
|
-
* 也**不收币种**:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但点位是无量纲的数,
|
|
64
|
-
* 替它标货币单位是替上游说话(`docs/design/watchlist-hk-us.md` §3.5)。
|
|
65
|
-
*
|
|
66
|
-
* 身份守卫是形状而非猜测:指数行第 2 位美股回 `.IXIC`(**前导点**)、港股回 `HSI`(**纯字母**);
|
|
67
|
-
* 个股行美股回 `AAPL.OQ`(点在中间)、港股回 `00700`(5 位数字)。2026-10-07 实测踩到过
|
|
68
|
-
* `usSSPX` 回的是 `SSPX.AM`(一只 ETF)——所以猜写法拿指数必错,而这一层的形状核对是最后一道闸。
|
|
69
|
-
*/
|
|
31
|
+
/** Indices: points only; magnitude columns unreadable. No currency. US code has a leading dot, HK is letters. */
|
|
70
32
|
export declare function parseOffshoreIndexRows(text: string, requested: Set<string>, market: 'hk' | 'us'): JsonRecord[];
|
|
71
|
-
/** 港美股指数代码:HK 是字母码(HSI / HSTECH / HSCEI),美是裸字母码(IXIC / DJI / INX,请求时不带那个前导点)。 */
|
|
72
33
|
export declare function normalizeOffshoreIndexCode(value: unknown, name: string, market: 'hk' | 'us'): string;
|
|
73
|
-
/**
|
|
74
|
-
* 港美股快照的**唯一**取数编排(能力侧与自选股面板共用)。
|
|
75
|
-
* 港股一次批量问完;美股先按原写法问一轮,**没回行**的那几只才用剥掉后缀的写法再问,
|
|
76
|
-
* 所以正常路径(纯字母 ticker)一次请求都不多花。
|
|
77
|
-
*/
|
|
34
|
+
/** HK one batch; US retries only no-row tickers with stripped suffixes. */
|
|
78
35
|
export declare function fetchOffshoreQuotes(market: 'hk' | 'us', codes: string[], signal: AbortSignal): Promise<JsonRecord[]>;
|
|
79
|
-
/** 港美股指数快照的取数编排:字母码没有"剥后缀重问"这一说,所以一轮问完。 */
|
|
80
36
|
export declare function fetchOffshoreIndexQuotes(market: 'hk' | 'us', codes: string[], signal: AbortSignal): Promise<JsonRecord[]>;
|
|
81
|
-
/** 美股 K 线入口需要的带后缀身份:先从快照拿上游自己的第 2 位(能力侧用,形状判据同一条)。 */
|
|
82
37
|
export declare function resolveUsKlineSymbol(ticker: string, signal: AbortSignal): Promise<string>;
|
|
83
|
-
/**
|
|
84
|
-
* smartbox(腾讯公开检索)的一行候选。
|
|
85
|
-
*
|
|
86
|
-
* 回包形状是 `v_hint="市场~代码~名称~拼音~类型^…"`:ASCII 的 `\uXXXX` 转义(**不需要 GBK**)、
|
|
87
|
-
* `^` 分行、`~` 分列,最多约 10 条。无命中与**非法 `t` 值**都回 `N` 这一个哨兵(实测
|
|
88
|
-
* `t=hk,us` 这种列表形态也落进 `N`),所以解析器把它当零条、调用方把 `t` 钉成常量。
|
|
89
|
-
*/
|
|
38
|
+
/** smartbox `v_hint="market~code~name~pinyin~type^…"` (~10); no hit or invalid `t` both return `N`. */
|
|
90
39
|
export interface SmartboxCandidate {
|
|
91
40
|
market: string;
|
|
92
41
|
code: string;
|
|
@@ -95,6 +44,5 @@ export interface SmartboxCandidate {
|
|
|
95
44
|
type: string;
|
|
96
45
|
}
|
|
97
46
|
export declare function parseSmartbox(text: string): SmartboxCandidate[];
|
|
98
|
-
/** smartbox 检索:市场段只能是上游认的单值(`hk` / `us` / `all`),列表形态实测失效。 */
|
|
99
47
|
export declare const SMARTBOX_MARKETS: string[];
|
|
100
48
|
export declare function fetchSmartbox(query: string, market: string, signal: AbortSignal): Promise<SmartboxCandidate[]>;
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 为什么单独一份:host 平面的自选股(`watchlist/index.js`)与插件侧的行情能力
|
|
5
|
-
* (`tencent-http.ts`)必须共用三样东西——同一条出口节流链、同一份港美股字段位序表、同一套守卫。
|
|
6
|
-
* 两条独立节流等于没有限流(东财那次的教训),两份位序表迟早只会修好一份(AGENTS.md §9.7)。
|
|
7
|
-
* 这一层一旦被别的模块拖进来,host 半边就会顺带加载整棵插件依赖树,所以这里不放能力定义、
|
|
8
|
-
* 不放 Dataset 依赖、也不放任何类型依赖。
|
|
9
|
-
*
|
|
10
|
-
* 错误文案与判定形状逐字保持历史形态(Agent 与用户看到的字符串不能因抽取而漂移)。
|
|
2
|
+
* Tencent public endpoint kernel: leaf (only `net/throttle.js`). Shared by the host-plane watchlist
|
|
3
|
+
* and `tencent-http.ts` for one throttle chain and one HK/US field table. No other dependencies.
|
|
11
4
|
*/
|
|
12
5
|
import { createRequestThrottle } from '../net/throttle.js';
|
|
13
6
|
export const QUOTE_URL = 'https://qt.gtimg.cn/q=';
|
|
14
7
|
export const SMARTBOX_URL = 'https://smartbox.gtimg.cn/s3/';
|
|
15
|
-
/**
|
|
16
|
-
* 腾讯出口的**唯一**节流链(进程级)。行情快照、分笔、K 线每一次换机、港美股快照、smartbox
|
|
17
|
-
* 检索全部过这一条链——它们打的是同一家,按 IP 风控(429 / 空 data / 静默限流)。
|
|
18
|
-
* 间隔取 120ms:与最早那版分笔翻页的节奏同量级。
|
|
19
|
-
*/
|
|
20
8
|
export const TENCENT_MIN_INTERVAL_MS = 120;
|
|
21
9
|
export const tencentThrottle = createRequestThrottle(TENCENT_MIN_INTERVAL_MS);
|
|
22
10
|
export function isRecord(value) {
|
|
@@ -45,10 +33,7 @@ async function readResponse(response, encoding) {
|
|
|
45
33
|
const bytes = await response.arrayBuffer();
|
|
46
34
|
return new TextDecoder(encoding).decode(bytes);
|
|
47
35
|
}
|
|
48
|
-
/**
|
|
49
|
-
* 腾讯出口的取文本:取消不排队(队首那个请求挂住时,把这次取消塞进队列等于把它吞掉,
|
|
50
|
-
* `docs/dev/web-retriever.md` §4.2「取消原样抛出」),排队与最小间隔由节流链负责。
|
|
51
|
-
*/
|
|
36
|
+
/** Cancellation does not queue; see `docs/dev/web-retriever.md` §4.2. */
|
|
52
37
|
export async function tencentGetText(url, signal, encoding = 'utf-8', init = {}) {
|
|
53
38
|
signal.throwIfAborted();
|
|
54
39
|
return tencentThrottle(async () => {
|
|
@@ -63,24 +48,14 @@ export async function tencentGetText(url, signal, encoding = 'utf-8', init = {})
|
|
|
63
48
|
return text;
|
|
64
49
|
});
|
|
65
50
|
}
|
|
66
|
-
/**
|
|
67
|
-
* 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
|
|
68
|
-
* 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
|
|
69
|
-
* 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
|
|
70
|
-
* 所以这两张表各自映射,并且只收能用算术或量级证明的列(docs/dev/tool-schema.md §10.2:口径猜错就是话说错)。
|
|
71
|
-
*/
|
|
51
|
+
/** Separate HK/US and A-share field tables: currency 75/35; turnover 59 (total shares) vs 38 (float). */
|
|
72
52
|
export const HK_QUOTE_FIELDS = 78;
|
|
73
53
|
export const US_QUOTE_FIELDS = 73;
|
|
74
|
-
/**
|
|
75
|
-
* 腾讯港美股时间戳是**交易所当地时间**。分隔符**按行漂移**:2026-10-05 那批同一份响应里,
|
|
76
|
-
* 一行给 `YYYY/MM/DD HH:mm:ss`、另一行给 `YYYY-MM-DD HH:mm:ss`——所以两种都吃,输出统一成横杠那一种。
|
|
77
|
-
* (写成占位符而不是真日期:这两个形状在源码里只差一个字符,肉眼读注释分不出来。)
|
|
78
|
-
*/
|
|
54
|
+
/** Exchange-local time; separator drifts per row (`/` vs `-`), normalize to hyphen. */
|
|
79
55
|
export function offshoreQuoteTime(value) {
|
|
80
56
|
const match = /^(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(String(value ?? '').trim());
|
|
81
57
|
return match ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}` : null;
|
|
82
58
|
}
|
|
83
|
-
/** 逐行拆出 `v_<symbol>="…"`;symbol 就是请求里写出去的那串,所以请求与回执能一对一核对。 */
|
|
84
59
|
export function snapshotRows(text, requested) {
|
|
85
60
|
const rows = [];
|
|
86
61
|
for (const line of text.split(';')) {
|
|
@@ -93,7 +68,7 @@ export function snapshotRows(text, requested) {
|
|
|
93
68
|
}
|
|
94
69
|
return rows;
|
|
95
70
|
}
|
|
96
|
-
/**
|
|
71
|
+
/** HK codes are 5 digits; `hk700`/`hk0700` are silently dropped upstream, so left-pad. */
|
|
97
72
|
export function normalizeHkCode(value, name) {
|
|
98
73
|
const raw = String(value ?? '').trim().toUpperCase();
|
|
99
74
|
const digits = /^(?:HK)?(\d{1,5})(?:\.HK)?$/.exec(raw)?.[1];
|
|
@@ -101,24 +76,14 @@ export function normalizeHkCode(value, name) {
|
|
|
101
76
|
throw new Error(`parameter ${name} must be a five-digit Hong Kong stock code (for example 00700, hk00700 or 700); HK index codes such as HSI/HSTECH are not supported here`);
|
|
102
77
|
return digits.padStart(5, '0');
|
|
103
78
|
}
|
|
104
|
-
/**
|
|
105
|
-
* 美股写法照原样收下(`AAPL` / `BRK.B` / `BABA.N` / `AAPL.OQ` 都是真实存在的说法),
|
|
106
|
-
* 消歧交给上游(见 `usSymbolVariants`):实测腾讯**快照只认不带后缀的写法**(`usBABA.N` 整行不回,
|
|
107
|
-
* `usBABA` 认),而 K 线只认**带正确后缀**的写法(`usAAPL` 回一列 2011 年的陌生序列、
|
|
108
|
-
* `usAAPL.NS` 静默 0 行)。点号既可能是级别码(BRK.B / BF.B)也可能是交易所后缀(.N / .OQ),
|
|
109
|
-
* 从字面上分不出来——所以不猜。
|
|
110
|
-
*/
|
|
79
|
+
/** Snapshot matches only bare US tickers (`usBABA.N` → no row); K-line needs the suffix (`usAAPL` → stale 2011 data). */
|
|
111
80
|
export function normalizeUsCode(value, name) {
|
|
112
81
|
const raw = String(value ?? '').trim().toUpperCase();
|
|
113
82
|
const match = /^(?:US)?([A-Z]{1,6}(?:\.[A-Z]{1,3}){0,2})$/.exec(raw);
|
|
114
83
|
if (!match)
|
|
115
84
|
throw new Error(`parameter ${name} must be a US equity ticker such as AAPL, BRK.B or BABA.N (a Tencent exchange suffix like .OQ is accepted and re-resolved from the snapshot)`);
|
|
116
|
-
// 两位以上的点号尾段(.OQ/.AM/.NS)在腾讯那里一定是交易所后缀,直接剥掉,于是 AAPL 与 AAPL.OQ
|
|
117
|
-
// 是同一只票、只请求一次。**单字母尾段**(BRK.B 的级别码 vs BABA.N 的交易所码)字面上分不开,
|
|
118
|
-
// 就照原样留着,交给上游逐个写法裁决——猜错一次只是多一轮请求,猜错第二次就是把数据挂错标的。
|
|
119
85
|
return match[1].replace(/\.[A-Z]{2,3}$/u, '');
|
|
120
86
|
}
|
|
121
|
-
/** 依次尝试的写法:原样 → 每次剥掉最后一段点号,最多剥两段(BRK.B.N → BRK.B → BRK)。 */
|
|
122
87
|
export function usSymbolVariants(ticker) {
|
|
123
88
|
const variants = [`us${ticker}`];
|
|
124
89
|
let current = ticker;
|
|
@@ -136,8 +101,6 @@ export function parseHkQuoteRows(text, requested) {
|
|
|
136
101
|
for (const { symbol, fields } of snapshotRows(text, requested)) {
|
|
137
102
|
if (fields.length < HK_QUOTE_FIELDS)
|
|
138
103
|
throw sourceError(`Tencent HK quote ${symbol} returned ${fields.length} fields, expected at least ${HK_QUOTE_FIELDS}`, 'tencent_invalid_response');
|
|
139
|
-
// 只收个股:指数行的第 6/37 位在实测里既不是"股"也不是"港元"(hkHSI 两列相等、量级像万元),
|
|
140
|
-
// 两种量纲不许进同一列;参数层已经拒掉字母指数,这里再核对一次上游没把行串错。
|
|
141
104
|
if (!/^\d{5}$/.test(fields[2] ?? ''))
|
|
142
105
|
throw sourceError(`Tencent HK quote ${symbol} is not a five-digit stock code (got ${JSON.stringify(fields[2] ?? '')})`, 'tencent_invalid_response');
|
|
143
106
|
const quoteTime = offshoreQuoteTime(fields[30]);
|
|
@@ -179,8 +142,6 @@ export function parseUsQuoteRows(text, requested) {
|
|
|
179
142
|
for (const { symbol, fields } of snapshotRows(text, requested)) {
|
|
180
143
|
if (fields.length < US_QUOTE_FIELDS)
|
|
181
144
|
throw sourceError(`Tencent US quote ${symbol} returned ${fields.length} fields, expected at least ${US_QUOTE_FIELDS}`, 'tencent_invalid_response');
|
|
182
|
-
// 上游在 2 号位回交易所后缀(AAPL.OQ / BABA.N),这是 K 线入口要用的身份,缺了就无法核对。
|
|
183
|
-
// 交易所后缀实测有**一个字母**的(NYSE:BABA.N / BRK.B.N),把下限写成 2 会把真代码判成畸形。
|
|
184
145
|
if (!/^[A-Z][A-Z.]{0,9}\.[A-Z]{1,3}$/.test(fields[2] ?? ''))
|
|
185
146
|
throw sourceError(`Tencent US quote ${symbol} has an unexpected code ${JSON.stringify(fields[2] ?? '')}`, 'tencent_invalid_response');
|
|
186
147
|
const quoteTime = offshoreQuoteTime(fields[30]);
|
|
@@ -215,25 +176,11 @@ export function parseUsQuoteRows(text, requested) {
|
|
|
215
176
|
const stale = row.amount_usd === 0 && row.price !== null && row.price === row.last_close;
|
|
216
177
|
row.is_stale = stale;
|
|
217
178
|
row.stale_reason = stale ? 'zero turnover and price equals previous close' : null;
|
|
218
|
-
// symbol 是"哪个写法换来了这一行",只给上面的重试判定用,不出现在行里。
|
|
219
179
|
rows.push({ symbol, row });
|
|
220
180
|
}
|
|
221
181
|
return rows;
|
|
222
182
|
}
|
|
223
|
-
/**
|
|
224
|
-
* **港美股指数快照**:只收点位那一族能证明的列。
|
|
225
|
-
*
|
|
226
|
-
* 为什么必须另开一张表(而不是复用个股那张):实测指数行的量纲列不可读——
|
|
227
|
-
* 港 `hkHSI` 的第 6 / 36 / 37 位两列相等且量级像万元,美 `usIXIC` 的第 37 位给了 2.1e14、
|
|
228
|
-
* 第 38 / 44 / 45 / 62 / 63 位是空。把这些列收进输出表就等于承诺一个我们证明不了的口径。
|
|
229
|
-
*
|
|
230
|
-
* 也**不收币种**:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但点位是无量纲的数,
|
|
231
|
-
* 替它标货币单位是替上游说话(`docs/design/watchlist-hk-us.md` §3.5)。
|
|
232
|
-
*
|
|
233
|
-
* 身份守卫是形状而非猜测:指数行第 2 位美股回 `.IXIC`(**前导点**)、港股回 `HSI`(**纯字母**);
|
|
234
|
-
* 个股行美股回 `AAPL.OQ`(点在中间)、港股回 `00700`(5 位数字)。2026-10-07 实测踩到过
|
|
235
|
-
* `usSSPX` 回的是 `SSPX.AM`(一只 ETF)——所以猜写法拿指数必错,而这一层的形状核对是最后一道闸。
|
|
236
|
-
*/
|
|
183
|
+
/** Indices: points only; magnitude columns unreadable. No currency. US code has a leading dot, HK is letters. */
|
|
237
184
|
export function parseOffshoreIndexRows(text, requested, market) {
|
|
238
185
|
const fieldFloor = market === 'hk' ? HK_QUOTE_FIELDS : US_QUOTE_FIELDS;
|
|
239
186
|
const codeOf = (code) => (market === 'hk' ? code : code.replace(/^./u, ''));
|
|
@@ -242,7 +189,6 @@ export function parseOffshoreIndexRows(text, requested, market) {
|
|
|
242
189
|
if (fields.length < fieldFloor)
|
|
243
190
|
throw sourceError(`Tencent ${market} index quote ${symbol} returned ${fields.length} fields, expected at least ${fieldFloor}`, 'tencent_invalid_response');
|
|
244
191
|
const raw = String(fields[2] ?? '');
|
|
245
|
-
// 港:字母指数码;美:`.字母`。两种都明确排除个股形状(数字 / 点在中间的后缀形式)。
|
|
246
192
|
const shapeOk = market === 'hk' ? /^[A-Z][A-Z0-9.]{1,9}$/u.test(raw) : /^\.[A-Z][A-Z0-9.]{1,9}$/u.test(raw);
|
|
247
193
|
if (!shapeOk)
|
|
248
194
|
throw sourceError(`Tencent ${market} index quote ${symbol} is not an index code (got ${JSON.stringify(raw)})`, 'tencent_invalid_response');
|
|
@@ -268,7 +214,6 @@ export function parseOffshoreIndexRows(text, requested, market) {
|
|
|
268
214
|
}
|
|
269
215
|
return rows;
|
|
270
216
|
}
|
|
271
|
-
/** 港美股指数代码:HK 是字母码(HSI / HSTECH / HSCEI),美是裸字母码(IXIC / DJI / INX,请求时不带那个前导点)。 */
|
|
272
217
|
export function normalizeOffshoreIndexCode(value, name, market) {
|
|
273
218
|
const raw = String(value ?? '').trim().toUpperCase().replace(/^(?:HK|US)/u, '');
|
|
274
219
|
const stem = raw.replace(/^\./u, '').replace(/\.(?:HK|US)$/u, '');
|
|
@@ -280,11 +225,7 @@ export function normalizeOffshoreIndexCode(value, name, market) {
|
|
|
280
225
|
}
|
|
281
226
|
return stem;
|
|
282
227
|
}
|
|
283
|
-
/**
|
|
284
|
-
* 港美股快照的**唯一**取数编排(能力侧与自选股面板共用)。
|
|
285
|
-
* 港股一次批量问完;美股先按原写法问一轮,**没回行**的那几只才用剥掉后缀的写法再问,
|
|
286
|
-
* 所以正常路径(纯字母 ticker)一次请求都不多花。
|
|
287
|
-
*/
|
|
228
|
+
/** HK one batch; US retries only no-row tickers with stripped suffixes. */
|
|
288
229
|
export async function fetchOffshoreQuotes(market, codes, signal) {
|
|
289
230
|
if (market === 'hk') {
|
|
290
231
|
const symbols = codes.map((code) => `hk${code}`);
|
|
@@ -303,8 +244,7 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
|
|
|
303
244
|
tried.add(symbol.toLowerCase());
|
|
304
245
|
const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
|
|
305
246
|
const found = parseUsQuoteRows(text, new Set(symbols.map((symbol) => symbol.toLowerCase())));
|
|
306
|
-
//
|
|
307
|
-
// 比不中就会对一只已经拿到数的票再问一轮剥短写法,白烧一次出网。
|
|
247
|
+
// Match case-insensitively; a miss would requery an already-resolved ticker.
|
|
308
248
|
const hit = new Set(found.map((item) => item.symbol.toLowerCase()));
|
|
309
249
|
rows.push(...found.map((item) => item.row));
|
|
310
250
|
for (let index = 0; index < symbols.length; index += 1) {
|
|
@@ -312,7 +252,7 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
|
|
|
312
252
|
pending[index] = [];
|
|
313
253
|
continue;
|
|
314
254
|
}
|
|
315
|
-
//
|
|
255
|
+
// Strip one dot-segment and retry; when nothing is left, upstream does not know the ticker.
|
|
316
256
|
const next = pending[index].find((variant) => !tried.has(variant.toLowerCase()));
|
|
317
257
|
if (next !== undefined)
|
|
318
258
|
pending[index] = [next, ...pending[index].filter((variant) => variant !== next)];
|
|
@@ -322,7 +262,6 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
|
|
|
322
262
|
throw sourceError(`Tencent returned no US quote rows for ${codes.join(',')}`, 'tencent_empty_response');
|
|
323
263
|
return rows;
|
|
324
264
|
}
|
|
325
|
-
/** 港美股指数快照的取数编排:字母码没有"剥后缀重问"这一说,所以一轮问完。 */
|
|
326
265
|
export async function fetchOffshoreIndexQuotes(market, codes, signal) {
|
|
327
266
|
const symbols = codes.map((code) => `${market}${code}`);
|
|
328
267
|
const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
|
|
@@ -331,7 +270,6 @@ export async function fetchOffshoreIndexQuotes(market, codes, signal) {
|
|
|
331
270
|
throw sourceError(`Tencent returned no ${market} index quote rows for ${symbols.join(',')}`, 'tencent_empty_response');
|
|
332
271
|
return rows;
|
|
333
272
|
}
|
|
334
|
-
/** 美股 K 线入口需要的带后缀身份:先从快照拿上游自己的第 2 位(能力侧用,形状判据同一条)。 */
|
|
335
273
|
export async function resolveUsKlineSymbol(ticker, signal) {
|
|
336
274
|
for (const symbol of usSymbolVariants(ticker)) {
|
|
337
275
|
const text = await tencentGetText(`${QUOTE_URL}${symbol}`, signal, 'gbk');
|
|
@@ -362,7 +300,6 @@ export function parseSmartbox(text) {
|
|
|
362
300
|
}
|
|
363
301
|
return candidates;
|
|
364
302
|
}
|
|
365
|
-
/** 回包里的名称是 `\uXXXX` 字面转义(实测),解一次转义;解不动就原样返回,不编造名称。 */
|
|
366
303
|
function unescapeHint(value) {
|
|
367
304
|
if (!value.includes('\\u'))
|
|
368
305
|
return value;
|
|
@@ -373,14 +310,12 @@ function unescapeHint(value) {
|
|
|
373
310
|
return value;
|
|
374
311
|
}
|
|
375
312
|
}
|
|
376
|
-
/** smartbox 检索:市场段只能是上游认的单值(`hk` / `us` / `all`),列表形态实测失效。 */
|
|
377
313
|
export const SMARTBOX_MARKETS = ['all', 'hk', 'us'];
|
|
378
314
|
export async function fetchSmartbox(query, market, signal) {
|
|
379
315
|
if (!SMARTBOX_MARKETS.includes(market))
|
|
380
316
|
throw new Error(`smartbox market must be one of ${SMARTBOX_MARKETS.join('/')}; got ${JSON.stringify(market)}(列表形态实测回 N 哨兵,等于查无此票)`);
|
|
381
317
|
const url = `${SMARTBOX_URL}?v=2&t=${encodeURIComponent(market)}&q=${encodeURIComponent(query)}`;
|
|
382
|
-
//
|
|
383
|
-
// 映射的是 windows-1252,不是 US-ASCII,拿来解 ASCII 正文是个埋着的坑。
|
|
318
|
+
// ASCII `\uXXXX` body: use utf-8; the 'ascii' label maps to windows-1252.
|
|
384
319
|
const text = await tencentGetText(url, signal);
|
|
385
320
|
return parseSmartbox(text);
|
|
386
321
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import type { DataSource } from '../data-collector/hub.js';
|
|
2
2
|
export type WindApiKeyResolver = () => Promise<string | undefined>;
|
|
3
|
-
/**
|
|
4
|
-
* §6.7 改口后的 P1 三条。`wind_edb_*` 是 P0(EDB 是唯一来源),`wind_stock_kline` 是 P1。
|
|
5
|
-
* 不接的一概有理由:price_indicators 与全部 NL 工具(中文键 / 列名是 question 的函数)、
|
|
6
|
-
* bond 与 fund/index(重叠或未测)、analytics_data(实测连 initialize 都挂)。
|
|
7
|
-
*/
|
|
8
3
|
export declare function createWindSources(resolveApiKey: WindApiKeyResolver): DataSource[];
|