@v587d/capital-generation 2.8.3 → 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 +54 -1
- package/README.md +40 -19
- 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 -34
- package/lib/sources/ftshare-rest.js +84 -292
- 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 +30 -66
- 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
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fuyao
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (`fuyao-rest.ts`)必须共用同一套 base URL、鉴权头、信封 `code` 判定与错误文案——
|
|
6
|
-
* "同一知识只有一份实现"(AGENTS.md §9.7)。这一层一旦被别的模块拖进来,host 半边
|
|
7
|
-
* 就会顺带加载整棵插件依赖树,所以这里不放校验器、能力定义、也不放任何类型依赖。
|
|
8
|
-
*
|
|
9
|
-
* 错误文案逐字保持历史形态(Agent 与用户看到的字符串不能因抽取而漂移);结构化的
|
|
10
|
-
* `kind` / `code` 是给自选股那一类需要映射成自己错误码的消费者用的。
|
|
2
|
+
* Fuyao request kernel: zero-import leaf shared by the host-plane watchlist package and
|
|
3
|
+
* `fuyao-rest.ts` (base URL, auth header, envelope codes, error text). Do not add validators,
|
|
4
|
+
* capability definitions, or type dependencies here: host code would load the plugin tree.
|
|
11
5
|
*/
|
|
12
6
|
export declare const DEFAULT_FUYAO_BASE_URL = "https://fuyao.aicubes.cn";
|
|
13
|
-
/** 单值参数长度上限;与历史 `MAX_PARAM_LENGTH` 同值。 */
|
|
14
7
|
export declare const FUYAO_MAX_PARAM_LENGTH = 2048;
|
|
15
8
|
export type FuyaoApiEnvelope = {
|
|
16
9
|
code?: number;
|
|
@@ -19,7 +12,7 @@ export type FuyaoApiEnvelope = {
|
|
|
19
12
|
data?: unknown;
|
|
20
13
|
};
|
|
21
14
|
export type FuyaoParams = Record<string, unknown>;
|
|
22
|
-
/**
|
|
15
|
+
/** Failure kinds: HTTP 429 and code=4001 are rate limits; `closed` is capability-level (retry is futile). */
|
|
23
16
|
export type FuyaoFailureKind = 'credential_missing' | 'rate_limited' | 'capability_closed' | 'invalid_parameter' | 'http_error' | 'upstream_error';
|
|
24
17
|
export declare class FuyaoError extends Error {
|
|
25
18
|
readonly kind: FuyaoFailureKind;
|
|
@@ -33,12 +26,9 @@ export declare class FuyaoError extends Error {
|
|
|
33
26
|
});
|
|
34
27
|
}
|
|
35
28
|
export declare function normalizeFuyaoBaseUrl(baseUrl: string): string;
|
|
36
|
-
/**
|
|
29
|
+
/** Send only declared params; reject bad type/length so misspelled keys are never silently dropped. */
|
|
37
30
|
export declare function fuyaoQuery(params: FuyaoParams, allowed: string[]): URLSearchParams;
|
|
38
|
-
/**
|
|
39
|
-
* 发一次 Fuyao 请求并返回信封。调用方自己决定如何消费 `data`;
|
|
40
|
-
* 非 0 信封、非 2xx HTTP、2004 能力封闭都以 `FuyaoError` 抛出,文案与历史一致。
|
|
41
|
-
*/
|
|
31
|
+
/** One Fuyao call returning the envelope; non-zero code, non-2xx and 2004 become `FuyaoError`. */
|
|
42
32
|
export declare function fuyaoRequest(input: {
|
|
43
33
|
baseUrl: string;
|
|
44
34
|
apiKey: string | undefined;
|
|
@@ -46,7 +36,7 @@ export declare function fuyaoRequest(input: {
|
|
|
46
36
|
search: URLSearchParams;
|
|
47
37
|
signal: AbortSignal;
|
|
48
38
|
}): Promise<FuyaoApiEnvelope>;
|
|
49
|
-
/**
|
|
39
|
+
/** Row array lives at `data.item`; `data` may be null. */
|
|
50
40
|
export declare function fuyaoItems(envelope: FuyaoApiEnvelope): FuyaoParams[];
|
|
51
|
-
/** `data.timestamp
|
|
41
|
+
/** `data.timestamp` in ms; upstream returns null when there is no data (do not zero-fill). */
|
|
52
42
|
export declare function fuyaoTimestamp(envelope: FuyaoApiEnvelope): number | null;
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fuyao
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (`fuyao-rest.ts`)必须共用同一套 base URL、鉴权头、信封 `code` 判定与错误文案——
|
|
6
|
-
* "同一知识只有一份实现"(AGENTS.md §9.7)。这一层一旦被别的模块拖进来,host 半边
|
|
7
|
-
* 就会顺带加载整棵插件依赖树,所以这里不放校验器、能力定义、也不放任何类型依赖。
|
|
8
|
-
*
|
|
9
|
-
* 错误文案逐字保持历史形态(Agent 与用户看到的字符串不能因抽取而漂移);结构化的
|
|
10
|
-
* `kind` / `code` 是给自选股那一类需要映射成自己错误码的消费者用的。
|
|
2
|
+
* Fuyao request kernel: zero-import leaf shared by the host-plane watchlist package and
|
|
3
|
+
* `fuyao-rest.ts` (base URL, auth header, envelope codes, error text). Do not add validators,
|
|
4
|
+
* capability definitions, or type dependencies here: host code would load the plugin tree.
|
|
11
5
|
*/
|
|
12
6
|
export const DEFAULT_FUYAO_BASE_URL = 'https://fuyao.aicubes.cn';
|
|
13
|
-
/** 单值参数长度上限;与历史 `MAX_PARAM_LENGTH` 同值。 */
|
|
14
7
|
export const FUYAO_MAX_PARAM_LENGTH = 2048;
|
|
15
8
|
export class FuyaoError extends Error {
|
|
16
9
|
kind;
|
|
@@ -29,7 +22,7 @@ export class FuyaoError extends Error {
|
|
|
29
22
|
export function normalizeFuyaoBaseUrl(baseUrl) {
|
|
30
23
|
return baseUrl.replace(/\/$/, '');
|
|
31
24
|
}
|
|
32
|
-
/**
|
|
25
|
+
/** Send only declared params; reject bad type/length so misspelled keys are never silently dropped. */
|
|
33
26
|
export function fuyaoQuery(params, allowed) {
|
|
34
27
|
const search = new URLSearchParams();
|
|
35
28
|
for (const name of allowed) {
|
|
@@ -46,10 +39,7 @@ export function fuyaoQuery(params, allowed) {
|
|
|
46
39
|
}
|
|
47
40
|
return search;
|
|
48
41
|
}
|
|
49
|
-
/**
|
|
50
|
-
* 发一次 Fuyao 请求并返回信封。调用方自己决定如何消费 `data`;
|
|
51
|
-
* 非 0 信封、非 2xx HTTP、2004 能力封闭都以 `FuyaoError` 抛出,文案与历史一致。
|
|
52
|
-
*/
|
|
42
|
+
/** One Fuyao call returning the envelope; non-zero code, non-2xx and 2004 become `FuyaoError`. */
|
|
53
43
|
export async function fuyaoRequest(input) {
|
|
54
44
|
if (!input.apiKey) {
|
|
55
45
|
throw new FuyaoError('credential_missing', 'FUYAO_API_KEY is not configured');
|
|
@@ -65,16 +55,15 @@ export async function fuyaoRequest(input) {
|
|
|
65
55
|
if (!response.ok)
|
|
66
56
|
throw new FuyaoError('http_error', `Fuyao HTTP error ${response.status}`, { status: response.status });
|
|
67
57
|
const envelope = await response.json();
|
|
68
|
-
// 2004 =
|
|
69
|
-
// 这是能力级不可用,不是瞬时故障:错误文案必须让调用方停止重试,否则会白烧配额与回合。
|
|
58
|
+
// 2004 = capability is AI-client-only and closed to external access; retrying burns quota.
|
|
70
59
|
if (envelope.code === 2004) {
|
|
71
60
|
throw new FuyaoError('capability_closed', 'Fuyao API error 2004: 该数据能力为同花顺 AI 客户端专用,当前未开放外部接入;不要重试,改用其他能力或如实告知用户该能力不可用', { code: 2004, requestId: envelope.request_id });
|
|
72
61
|
}
|
|
73
|
-
// 4001 =
|
|
62
|
+
// 4001 = rate limited; official guidance says do not retry immediately.
|
|
74
63
|
if (envelope.code === 4001) {
|
|
75
64
|
throw new FuyaoError('rate_limited', `Fuyao API error 4001: ${envelope.message ?? 'request failed'}${envelope.request_id ? ` (request_id: ${envelope.request_id})` : ''}`, { code: 4001, requestId: envelope.request_id });
|
|
76
65
|
}
|
|
77
|
-
// 1002 / 1003 =
|
|
66
|
+
// 1002 / 1003 = parameter format/domain errors; watchlist maps these to its own codes.
|
|
78
67
|
if (envelope.code === 1002 || envelope.code === 1003) {
|
|
79
68
|
throw new FuyaoError('invalid_parameter', `Fuyao API error ${envelope.code}: ${envelope.message ?? 'request failed'}${envelope.request_id ? ` (request_id: ${envelope.request_id})` : ''}`, { code: envelope.code, requestId: envelope.request_id });
|
|
80
69
|
}
|
|
@@ -83,13 +72,13 @@ export async function fuyaoRequest(input) {
|
|
|
83
72
|
}
|
|
84
73
|
return envelope;
|
|
85
74
|
}
|
|
86
|
-
/**
|
|
75
|
+
/** Row array lives at `data.item`; `data` may be null. */
|
|
87
76
|
export function fuyaoItems(envelope) {
|
|
88
77
|
const data = envelope.data;
|
|
89
78
|
const rows = data?.item;
|
|
90
79
|
return Array.isArray(rows) ? rows : [];
|
|
91
80
|
}
|
|
92
|
-
/** `data.timestamp
|
|
81
|
+
/** `data.timestamp` in ms; upstream returns null when there is no data (do not zero-fill). */
|
|
93
82
|
export function fuyaoTimestamp(envelope) {
|
|
94
83
|
const data = envelope.data;
|
|
95
84
|
return typeof data?.timestamp === 'number' ? data.timestamp : null;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { DataSource } from '../data-collector/hub.js';
|
|
3
|
-
/**
|
|
3
|
+
/** API key resolved per execution; credentials service caches nothing across calls. */
|
|
4
4
|
export type FuyaoApiKeyResolver = () => Promise<string | undefined>;
|
|
5
|
-
/** 批量代码接口的原始 token 上限(按文档:去重前校验)。 */
|
|
6
5
|
declare const MAX_BATCH_CODES = 100;
|
|
7
|
-
/**
|
|
8
|
-
* 历史 K 线 / 财务时间区间上限:上游规定 end - start 不超过 10 年(超出返回 1003)。
|
|
9
|
-
* 本地用 3660 天(含闰日冗余)保守前置拦截,只挡明显越界,不与上游口径冲突。
|
|
10
|
-
*/
|
|
6
|
+
/** Upstream max end - start is 10 years (overrun returns 1003); 3660 days is a conservative pre-check. */
|
|
11
7
|
declare const MAX_HISTORY_WINDOW_MS: number;
|
|
12
8
|
export declare function createFuyaoRestSources(resolveApiKey: FuyaoApiKeyResolver, baseUrl?: string): DataSource[];
|
|
13
9
|
export declare function resolveFuyaoApiKey(ctx: Context, credentialRef?: string): Promise<string | undefined>;
|
|
@@ -3,27 +3,13 @@ import { getDataTimeContract } from '../time/tools.js';
|
|
|
3
3
|
import { DEFAULT_FUYAO_BASE_URL, FUYAO_MAX_PARAM_LENGTH, fuyaoQuery, fuyaoRequest } from './fuyao-core.js';
|
|
4
4
|
const DEFAULT_BASE_URL = DEFAULT_FUYAO_BASE_URL;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 并且必须幂等:Hub 会在入队前调用一次(用于 merge key 与 params_digest),
|
|
10
|
-
* execute 再调用一次(直接调用 source 时也要生效)。
|
|
11
|
-
* - 规范化在 digest 之前发生,保证「语义相同、书写不同」的请求(大小写、空白、
|
|
12
|
-
* 逗号列表中的重复项、可忽略的默认值)命中同一个 Dataset,而不是各写一份。
|
|
13
|
-
* - 所有错误都是本地错误:不发请求、不落盘、不做网络层猜测。
|
|
14
|
-
* - **不注入上游默认值**:省略参数与显式传默认值是两种请求形态,各自的 digest 稳定;
|
|
15
|
-
* 把上游默认值写进本地代码,会让 digest 的含义随上游默认值漂移而悄悄改变。
|
|
6
|
+
* normalize must be idempotent: Hub calls it before enqueue (digest) and execute calls it again.
|
|
7
|
+
* Canonicalize before the digest so equivalent spellings share one Dataset; never inject upstream
|
|
8
|
+
* defaults, which would make omitted and explicit defaults drift apart.
|
|
16
9
|
*/
|
|
17
|
-
/** A 股完整代码:六位数字 + 交易所后缀。裸代码会被拒绝(上游禁止自行拼接后缀)。 */
|
|
18
10
|
const A_SHARE_CODE = /^\d{6}\.(SH|SZ|BJ)$/;
|
|
19
|
-
/** 指数代码:交易所指数(`.SH`/`.SZ`)与同花顺指数(`.TI`)。 */
|
|
20
11
|
const INDEX_CODE = /^\d{6}\.(SH|SZ|BJ|TI)$/;
|
|
21
12
|
const EXCHANGES = ['SH', 'SZ', 'BJ'];
|
|
22
|
-
/**
|
|
23
|
-
* ticker_search 的交易所与资产类型**按官方契约全量放开**(含期货/期权交易所与 futures/options)。
|
|
24
|
-
* 注意这是元数据域:本 preset 主动不接入期货/期权的**数据**端点,但"能否消歧一个代码"与
|
|
25
|
-
* "能否取到它的行情"是两件事——本地收窄枚举只会让上游支持的查询在这里莫名失败。
|
|
26
|
-
*/
|
|
27
13
|
const SEARCH_EXCHANGES = ['SH', 'SZ', 'BJ', 'SSE', 'SZSE', 'CFFEX', 'SHFE', 'INE', 'DCE', 'CZCE', 'GFEX'];
|
|
28
14
|
const SEARCH_ASSET_TYPES = ['a-share', 'a-share-index', 'forex', 'fund-otc', 'fund-etf', 'fund-lof', 'fund-reits', 'futures', 'options'];
|
|
29
15
|
const CATALOG_ASSET_TYPES = ['a-share', 'a-share-index', 'fund-otc', 'fund-etf', 'fund-lof', 'fund-reits', 'forex', 'futures', 'options'];
|
|
@@ -39,31 +25,22 @@ const ANOMALY_TAGS = ['LIMIT_UP', 'LIMIT_DOWN', 'SHARP_RISE', 'SHARP_FALL', 'RAP
|
|
|
39
25
|
const DRAGON_TIGER_BOARDS = ['all', 'org', 'hot_money'];
|
|
40
26
|
const LIMIT_DOWN_SORT_FIELDS = ['last_limit_time', 'first_limit_time', 'last_price', 'price_change_ratio_pct', 'turnover_ratio_pct'];
|
|
41
27
|
const LIMIT_BREAK_SORT_FIELDS = ['price_change_ratio_pct', 'open_times', 'last_price', 'turnover_ratio_pct', 'turnover'];
|
|
42
|
-
/** 个股异动批量查询的原始 token 上限(按文档:去重前校验)。 */
|
|
43
28
|
const MAX_ANOMALY_CODES = 50;
|
|
44
|
-
/** 基金代码:六位数字 + 市场后缀(.OF 场外 / .SH / .SZ)。 */
|
|
45
29
|
const FUND_CODE = /^\d{6}\.(OF|SH|SZ)$/;
|
|
46
30
|
const NAV_RANGES = ['week', 'month', 'tmonth', 'hyear', 'year', 'twoyear', 'tyear', 'fyear'];
|
|
47
31
|
const NAV_TYPES = ['unit', 'adj', 'unit,adj'];
|
|
48
32
|
const MERGE_SCOPES = ['all', 'merged', 'separate'];
|
|
49
|
-
/**
|
|
33
|
+
/** Fund on-exchange K-line window: 5 natural years max, not the A-share 10. */
|
|
50
34
|
const MAX_FUND_HISTORY_WINDOW_MS = 1830 * 24 * 60 * 60 * 1000;
|
|
51
|
-
/**
|
|
35
|
+
/** Top-10 holders limit max is 10; 0/11/201 return 1003. */
|
|
52
36
|
const MAX_TOP_HOLDERS_LIMIT = 10;
|
|
53
|
-
/** 基金经理区间收益的 range 枚举(与 nav 的 8 值枚举不同,只有 5 个)。 */
|
|
54
37
|
const MANAGER_RANGES = ['month', 'tmonth', 'year', 'nowyear', 'now'];
|
|
55
38
|
const OFFERING_SUBSCRIBE = ['active', 'upcoming'];
|
|
56
|
-
/** 单值参数长度上限;与 `fuyao-core` 的查询串硬上限同值(同一份知识)。 */
|
|
57
39
|
const MAX_PARAM_LENGTH = FUYAO_MAX_PARAM_LENGTH;
|
|
58
40
|
const MAX_TEXT_LENGTH = 512;
|
|
59
|
-
/** 批量代码接口的原始 token 上限(按文档:去重前校验)。 */
|
|
60
41
|
const MAX_BATCH_CODES = 100;
|
|
61
|
-
/**
|
|
62
|
-
* 历史 K 线 / 财务时间区间上限:上游规定 end - start 不超过 10 年(超出返回 1003)。
|
|
63
|
-
* 本地用 3660 天(含闰日冗余)保守前置拦截,只挡明显越界,不与上游口径冲突。
|
|
64
|
-
*/
|
|
42
|
+
/** Upstream max end - start is 10 years (overrun returns 1003); 3660 days is a conservative pre-check. */
|
|
65
43
|
const MAX_HISTORY_WINDOW_MS = 3660 * 24 * 60 * 60 * 1000;
|
|
66
|
-
/** 递归按键排序的规范化 JSON:语义相同、键序不同的 JSON 参数会归一到同一个 digest。 */
|
|
67
44
|
function canonicalJson(value) {
|
|
68
45
|
if (value === null || typeof value !== 'object')
|
|
69
46
|
return JSON.stringify(value);
|
|
@@ -72,11 +49,6 @@ function canonicalJson(value) {
|
|
|
72
49
|
const record = value;
|
|
73
50
|
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
74
51
|
}
|
|
75
|
-
/**
|
|
76
|
-
* 需要 URL 编码 JSON 传参的端点(backtest 条件、indicators 选择器、quota 分类)。
|
|
77
|
-
* 校验只做"能解析 + 顶层形状正确 + 长度受限",不臆造上游的内部字段白名单;
|
|
78
|
-
* 规范化后重新序列化,保证等价 JSON 命中同一 Dataset。
|
|
79
|
-
*/
|
|
80
52
|
function jsonParam(value, name, kind, maxLength = MAX_PARAM_LENGTH) {
|
|
81
53
|
const raw = requiredText(value, name, maxLength);
|
|
82
54
|
let parsed;
|
|
@@ -99,7 +71,6 @@ function optionalJsonParam(value, name, kind) {
|
|
|
99
71
|
return undefined;
|
|
100
72
|
return jsonParam(value, name, kind);
|
|
101
73
|
}
|
|
102
|
-
/** 小写枚举的统一规范化:`LIVE` 与 `live` 是同一个请求。 */
|
|
103
74
|
const lowerCase = (input) => input.toLowerCase();
|
|
104
75
|
function fail(message) {
|
|
105
76
|
throw new Error(message);
|
|
@@ -112,7 +83,6 @@ function paramsObject(value) {
|
|
|
112
83
|
function isRecord(value) {
|
|
113
84
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
114
85
|
}
|
|
115
|
-
/** 未知参数一律拒绝:normalize 会重建参数对象,放行等于静默丢弃模型的拼写错误。 */
|
|
116
86
|
function assertKnownParameters(values, allowed) {
|
|
117
87
|
for (const name of Object.keys(values)) {
|
|
118
88
|
if (!allowed.includes(name))
|
|
@@ -171,7 +141,6 @@ function requiredEnum(value, name, allowed, normalize) {
|
|
|
171
141
|
fail(`missing required parameter: ${name}`);
|
|
172
142
|
return enumValue(value, name, allowed, normalize);
|
|
173
143
|
}
|
|
174
|
-
/** `YYYY-MM-DD` 日期(企业行为事件的 from/to):格式与日历有效性都校验。 */
|
|
175
144
|
function dateString(value, name) {
|
|
176
145
|
const raw = text(value, name, 32);
|
|
177
146
|
const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
@@ -191,7 +160,7 @@ function dateString(value, name) {
|
|
|
191
160
|
fail(`parameter ${name} is outside the supported date range`);
|
|
192
161
|
return raw;
|
|
193
162
|
}
|
|
194
|
-
/**
|
|
163
|
+
/** `yyyy-1`..`yyyy-4` = Q1 / interim / Q3 / annual. */
|
|
195
164
|
function reportPeriod(value, name) {
|
|
196
165
|
const raw = text(value, name, 16);
|
|
197
166
|
const matched = /^(\d{4})-([1-4])$/.exec(raw);
|
|
@@ -201,7 +170,6 @@ function reportPeriod(value, name) {
|
|
|
201
170
|
fail(`parameter ${name} is outside the supported period range`);
|
|
202
171
|
return raw;
|
|
203
172
|
}
|
|
204
|
-
/** 规范化单个 A 股代码:trim + 大写 + 后缀校验。 */
|
|
205
173
|
function codeToken(value, name) {
|
|
206
174
|
const raw = text(value, name, 32).toUpperCase();
|
|
207
175
|
if (!A_SHARE_CODE.test(raw)) {
|
|
@@ -209,7 +177,6 @@ function codeToken(value, name) {
|
|
|
209
177
|
}
|
|
210
178
|
return raw;
|
|
211
179
|
}
|
|
212
|
-
/** 规范化单个指数代码(允许同花顺 `.TI`)。 */
|
|
213
180
|
function indexCodeToken(value, name) {
|
|
214
181
|
const raw = text(value, name, 32).toUpperCase();
|
|
215
182
|
if (!INDEX_CODE.test(raw)) {
|
|
@@ -217,10 +184,7 @@ function indexCodeToken(value, name) {
|
|
|
217
184
|
}
|
|
218
185
|
return raw;
|
|
219
186
|
}
|
|
220
|
-
/**
|
|
221
|
-
* 规范化逗号分隔代码列表:去空白、去重、保序;空串与非法代码都拒绝。
|
|
222
|
-
* `maximum` 按文档口径在**去重前**校验原始 token 数。
|
|
223
|
-
*/
|
|
187
|
+
/** `maximum` counts raw tokens before dedupe. */
|
|
224
188
|
function codeList(value, name, options = {}) {
|
|
225
189
|
const raw = requiredText(value, name, MAX_PARAM_LENGTH);
|
|
226
190
|
const tokens = raw.split(',').map((token) => token.trim()).filter((token) => token.length > 0);
|
|
@@ -238,7 +202,6 @@ function codeList(value, name, options = {}) {
|
|
|
238
202
|
}
|
|
239
203
|
return unique.join(',');
|
|
240
204
|
}
|
|
241
|
-
/** 逗号分隔的枚举列表:小写、去重、保序;任一非法值整体拒绝。 */
|
|
242
205
|
function enumList(value, name, allowed) {
|
|
243
206
|
const raw = requiredText(value, name, MAX_TEXT_LENGTH);
|
|
244
207
|
const unique = [];
|
|
@@ -255,11 +218,7 @@ function enumList(value, name, allowed) {
|
|
|
255
218
|
function objectSchema(properties, required = [], description) {
|
|
256
219
|
return { type: 'object', properties, required, additionalProperties: false, ...(description ? { description } : {}) };
|
|
257
220
|
}
|
|
258
|
-
/**
|
|
259
|
-
* 能力执行侧的信封调用:请求与 `code` 判定住在 `fuyao-core.ts`(host 平面的自选股
|
|
260
|
-
* 与这里共用一份),本层只把它恢复成 Hub 期望的 `{ data, schema }` 形态。
|
|
261
|
-
* 错误文案逐字保持历史形态(2004 / 4001 / 1002 那几条都会透到 Agent 眼前)。
|
|
262
|
-
*/
|
|
221
|
+
/** Envelope call lives in `fuyao-core.ts`; error texts are passed through verbatim. */
|
|
263
222
|
async function callFuyao(baseUrl, apiKey, path, params, signal, outputSchema) {
|
|
264
223
|
const envelope = await fuyaoRequest({ baseUrl, apiKey, path, search: params, signal });
|
|
265
224
|
return { data: envelope.data ?? null, schema: outputSchema };
|
|
@@ -285,15 +244,13 @@ function matchesField(value, spec) {
|
|
|
285
244
|
case 'array':
|
|
286
245
|
return Array.isArray(value);
|
|
287
246
|
case 'json':
|
|
288
|
-
//
|
|
289
|
-
// (实测基金诊断的 dimensions/resilience 就是数组)。只拒绝 undefined。
|
|
247
|
+
// Documentation says object but upstream may return an array or null; reject only undefined.
|
|
290
248
|
return true;
|
|
291
249
|
}
|
|
292
250
|
}
|
|
293
251
|
function matchesFields(record, fields) {
|
|
294
252
|
return Object.keys(fields).every((name) => matchesField(record[name], fields[name]));
|
|
295
253
|
}
|
|
296
|
-
/** envelope 元数据:字段可缺席或为 null;出现时必须是可用的整数。 */
|
|
297
254
|
function optionalInteger(value) {
|
|
298
255
|
if (value === undefined || value === null)
|
|
299
256
|
return true;
|
|
@@ -303,7 +260,6 @@ function optionalInteger(value) {
|
|
|
303
260
|
return Number.isSafeInteger(Number(value));
|
|
304
261
|
return false;
|
|
305
262
|
}
|
|
306
|
-
/** 输出护栏:只拒绝结构不可用的响应,不做业务推断。 */
|
|
307
263
|
function buildGuard(spec) {
|
|
308
264
|
return (data) => {
|
|
309
265
|
if (spec.dataArray) {
|
|
@@ -311,8 +267,6 @@ function buildGuard(spec) {
|
|
|
311
267
|
return false;
|
|
312
268
|
return data.every((row) => isRecord(row) && matchesFields(row, spec.dataArray));
|
|
313
269
|
}
|
|
314
|
-
// custom-only:顶层可能是数组(quota/list、indicators/line、indicators/table),
|
|
315
|
-
// 由校验器自己判断形状,不能在这里先做 isRecord 拦截。
|
|
316
270
|
if (spec.custom && !spec.data && !spec.item)
|
|
317
271
|
return spec.custom(data);
|
|
318
272
|
if (!isRecord(data))
|
|
@@ -333,7 +287,7 @@ function buildGuard(spec) {
|
|
|
333
287
|
return true;
|
|
334
288
|
};
|
|
335
289
|
}
|
|
336
|
-
/**
|
|
290
|
+
/** Value stays a raw numeric string to preserve precision. */
|
|
337
291
|
function validateAbilities(data) {
|
|
338
292
|
if (!isRecord(data))
|
|
339
293
|
return false;
|
|
@@ -349,15 +303,12 @@ function validateAbilities(data) {
|
|
|
349
303
|
&& (indicator.value === null || typeof indicator.value === 'string'));
|
|
350
304
|
});
|
|
351
305
|
}
|
|
352
|
-
// timestamp 与 total 在无有效数据时可以是 null(文档明确,如基金行情快照);运行时护栏一直放行 null,
|
|
353
|
-
// 模型可见 schema 也必须一致,否则 describe_capability 给出的契约是错的。
|
|
354
306
|
const nullableInteger = { oneOf: [{ type: 'integer' }, { type: 'null' }] };
|
|
355
307
|
const envelopeData = (item) => ({ type: 'object', properties: { timestamp: nullableInteger, total: nullableInteger, item }, additionalProperties: true });
|
|
356
308
|
const searchOutput = envelopeData({ type: 'array', items: { type: 'object', properties: { thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' }, exchange: { type: 'string' }, asset_type: { type: 'string' }, currency: { type: 'string' } }, additionalProperties: true } });
|
|
357
309
|
const snapshotOutput = envelopeData({ type: 'array', items: { type: 'object', properties: { thscode: { type: 'string' }, ticker: { type: 'string' }, last_price: { type: 'number' }, price_change: { type: 'number' }, price_change_ratio_pct: { type: 'number' }, open_price: { type: 'number' }, high_price: { type: 'number' }, low_price: { type: 'number' }, prev_price: { type: 'number' }, volume: { type: 'number' }, turnover: { type: 'number' } }, additionalProperties: true } });
|
|
358
310
|
const klineOutput = envelopeData({ type: 'array', items: { type: 'object', properties: { date_ms: { type: 'integer' }, open_price: { type: 'number' }, high_price: { type: 'number' }, low_price: { type: 'number' }, close_price: { type: 'number' }, volume: { type: 'number' }, turnover: { type: 'number' } }, additionalProperties: true } });
|
|
359
311
|
const calendarOutput = envelopeData({ type: 'array', items: { type: 'object', properties: { date_ms: { type: 'integer' }, date: { type: 'string' } }, additionalProperties: true } });
|
|
360
|
-
/** 行契约 -> 输出 schema 的字段描述(模型可见,只描述字段名与类型)。 */
|
|
361
312
|
function rowSchema(fields) {
|
|
362
313
|
const properties = {};
|
|
363
314
|
for (const [name, spec] of Object.entries(fields)) {
|
|
@@ -382,10 +333,8 @@ const indexCatalogFields = { thscode: 'text', name: 'text?' };
|
|
|
382
333
|
const indexConstituentFields = { thscode: 'text', ticker: 'text?', name: 'text?' };
|
|
383
334
|
const indexQuoteFields = { thscode: 'text', ticker: 'text?', last_price: 'number?', price_change: 'number?', price_change_ratio_pct: 'number?', open_price: 'number?', high_price: 'number?', low_price: 'number?', prev_price: 'number?', volume: 'number?', turnover: 'number?' };
|
|
384
335
|
const indexHistoryFields = { date_ms: 'number', open_price: 'number?', high_price: 'number?', low_price: 'number?', close_price: 'number?', volume: 'number?', turnover: 'number?' };
|
|
385
|
-
/** 财务三表共有的期间标识字段(严格)与币种(宽容)。 */
|
|
386
336
|
const limitDownFields = { thscode: 'text', ticker: 'text?', name: 'text?', last_price: 'number?', price_change_ratio_pct: 'number?', first_limit_time: 'text?', last_limit_time: 'text?', turnover_ratio_pct: 'number?' };
|
|
387
337
|
const limitBreakFields = { thscode: 'text', ticker: 'text?', name: 'text?', last_price: 'number?', price_change_ratio_pct: 'number?', open_times: 'number?', turnover_ratio_pct: 'number?', turnover: 'number?' };
|
|
388
|
-
/** 飙升榜与热股榜共用字段;heat 是上游原始**字符串**(保留精度,不转 number)。 */
|
|
389
338
|
const rankListFields = { thscode: 'text', ticker: 'text?', name: 'text?', rank: 'number?', heat: 'text?', rank_change: 'number?', rank_trend: 'text?' };
|
|
390
339
|
const hotStockHistoryFields = { thscode: 'text', ticker: 'text?', name: 'text?', rank: 'number?' };
|
|
391
340
|
const rankTrendFields = { thscode: 'text', ticker: 'text?', date: 'text', date_ms: 'number?', rank: 'number?' };
|
|
@@ -397,10 +346,8 @@ const fundQuoteFields = { thscode: 'text', ticker: 'text?', last_price: 'number?
|
|
|
397
346
|
const fundHistoryFields = { date_ms: 'number', open_price: 'number?', high_price: 'number?', low_price: 'number?', close_price: 'number?', volume: 'number?', turnover: 'number?' };
|
|
398
347
|
const fundNavFields = { nav_date: 'number', unit_nav: 'number?', adj_nav: 'number?' };
|
|
399
348
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* 因此全部字段设为可选,另用 validateFundReturns 要求"至少命中一个收益/排名族字段",
|
|
403
|
-
* 既不放行垃圾结构,也不会误杀官方合法响应。
|
|
349
|
+
* No identity field and any single `return_*` may be absent (official example has 8, no return_week);
|
|
350
|
+
* all fields optional, but a row needs one return/rank field.
|
|
404
351
|
*/
|
|
405
352
|
const fundReturnsFields = {
|
|
406
353
|
return_week: 'number?', return_month: 'number?', return_tmonth: 'number?', return_hyear: 'number?',
|
|
@@ -413,7 +360,6 @@ const fundReturnsFields = {
|
|
|
413
360
|
rank_total_week: 'number?', rank_total_month: 'number?', rank_total_tmonth: 'number?', rank_total_hyear: 'number?',
|
|
414
361
|
rank_total_year: 'number?', rank_total_twoyear: 'number?', rank_total_tyear: 'number?', rank_total_fyear: 'number?',
|
|
415
362
|
};
|
|
416
|
-
/** 收益/排名族的字段前缀;一行至少命中一个才算可用,其余情况判契约不符。 */
|
|
417
363
|
const FUND_RETURN_PREFIXES = ['return_', 'peer_average_', 'rank_'];
|
|
418
364
|
function validateFundReturns(data) {
|
|
419
365
|
if (!isRecord(data) || !Array.isArray(data.item))
|
|
@@ -443,10 +389,9 @@ const fundFinancialIndicatorFields = { ...fundFinancialDateFields, ...fundNumber
|
|
|
443
389
|
const fundDividendFields = { per_ten_cash_before_tax: 'number?', per_ten_cash_after_tax: 'number?', progress: 'text?', publish_date_ms: 'number?', registration_date_ms: 'number?', ex_dividend_date_ms: 'number?', payment_date_ms: 'number?', reinvestment_date_ms: 'number?', profit_base_date_ms: 'number?', in_dividend_date_ms: 'number?' };
|
|
444
390
|
const fundOfferingFields = { thscode: 'text', ticker: 'text?', subscription_start_ms: 'number?', subscription_end_ms: 'number?' };
|
|
445
391
|
const fundQuotaSummaryFields = { name: 'text', unlimited: 'text?', total_limit: 'text?', total: 'text?', buy: 'text?' };
|
|
446
|
-
/**
|
|
392
|
+
/** dimensions/peer_* /resilience are arrays and probabilities/ranges null, despite docs saying object. */
|
|
447
393
|
const fundDiagnosticsFields = { thscode: 'text', ticker: 'text?', fund_type: 'text?', peer_code: 'text?', dimensions: 'json?', peer_dimensions: 'json?', probabilities: 'json?', ranges: 'json?', resilience: 'json?', peer_resilience: 'json?' };
|
|
448
394
|
const fundBacktestIndicatorFields = { indicator_code: 'text', indicator_name: 'text?', id: 'number?', type: 'number?', value_kind: 'number?', support_operation: 'text?', unit: 'text?', description: 'text?', state_rules: 'text?', create_time: 'text?', update_time: 'text?' };
|
|
449
|
-
/** quota/list 是三层嵌套(data[] → sub_tab[] → fund_list[]),没有 item[] 信封。 */
|
|
450
395
|
function validateQuotaList(data) {
|
|
451
396
|
if (!Array.isArray(data))
|
|
452
397
|
return false;
|
|
@@ -467,7 +412,6 @@ function validateQuotaList(data) {
|
|
|
467
412
|
});
|
|
468
413
|
});
|
|
469
414
|
}
|
|
470
|
-
/** indicators/line:time_range 时间轴 + indexes 元信息 + data 序列,三层结构。 */
|
|
471
415
|
function validateIndicatorLine(data) {
|
|
472
416
|
if (!isRecord(data))
|
|
473
417
|
return false;
|
|
@@ -485,7 +429,6 @@ function validateIndicatorLine(data) {
|
|
|
485
429
|
return series.values.every((value) => isRecord(value) && Number.isSafeInteger(value.idx));
|
|
486
430
|
});
|
|
487
431
|
}
|
|
488
|
-
/** indicators/table:total + indexes + data[],值可为 JSON 或 null。 */
|
|
489
432
|
function validateIndicatorTable(data) {
|
|
490
433
|
if (!isRecord(data))
|
|
491
434
|
return false;
|
|
@@ -516,7 +459,7 @@ function normalizeQuote(params) {
|
|
|
516
459
|
const normalized = {};
|
|
517
460
|
if (params.thscodes !== undefined) {
|
|
518
461
|
normalized.thscodes = codeList(params.thscodes, 'thscodes');
|
|
519
|
-
//
|
|
462
|
+
// Upstream ignores paging with thscodes; drop it so equivalent requests share one Dataset.
|
|
520
463
|
return normalized;
|
|
521
464
|
}
|
|
522
465
|
if (params.limit !== undefined)
|
|
@@ -525,7 +468,6 @@ function normalizeQuote(params) {
|
|
|
525
468
|
normalized.offset = integer(params.offset, 'offset', { minimum: 0 });
|
|
526
469
|
return normalized;
|
|
527
470
|
}
|
|
528
|
-
/** 时间区间型请求的共用校验:成对出现、end >= start、窗口不超过 10 年。 */
|
|
529
471
|
function timeWindow(params, normalized) {
|
|
530
472
|
const { start, end } = params;
|
|
531
473
|
if ((start === undefined) !== (end === undefined))
|
|
@@ -592,7 +534,6 @@ function normalizeCorporateActions(params) {
|
|
|
592
534
|
normalized.to = to;
|
|
593
535
|
return normalized;
|
|
594
536
|
}
|
|
595
|
-
/** 财务三表共用参数:最近 N 期(limit)与时间区间(start+end)互斥、二选一。 */
|
|
596
537
|
function normalizeFinancial(params) {
|
|
597
538
|
const normalized = {
|
|
598
539
|
thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
|
|
@@ -648,7 +589,6 @@ function normalizeIndexConstituents(params) {
|
|
|
648
589
|
function normalizeIndexQuote(params) {
|
|
649
590
|
return { thscodes: codeList(params.thscodes, 'thscodes', { kind: 'index' }) };
|
|
650
591
|
}
|
|
651
|
-
/** 基金代码规范化:trim + 大写 + 市场后缀校验(不接受逗号与裸代码)。 */
|
|
652
592
|
function fundCodeToken(value, name) {
|
|
653
593
|
const raw = text(value, name, 32).toUpperCase();
|
|
654
594
|
if (!FUND_CODE.test(raw)) {
|
|
@@ -656,11 +596,9 @@ function fundCodeToken(value, name) {
|
|
|
656
596
|
}
|
|
657
597
|
return raw;
|
|
658
598
|
}
|
|
659
|
-
/** 基金经理 / 基金公司标识:格式上游未公开,只做非空与长度约束。 */
|
|
660
599
|
function identifierToken(value, name) {
|
|
661
600
|
return requiredText(value, name, 64);
|
|
662
601
|
}
|
|
663
|
-
/** 只带 thscode 的基金端点(基本资料、业绩快照、持仓、持有人等)。 */
|
|
664
602
|
function onlyFundCode(params) {
|
|
665
603
|
return { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
|
|
666
604
|
}
|
|
@@ -689,8 +627,8 @@ function normalizeFundNav(params) {
|
|
|
689
627
|
function normalizeFundStockHistory(params) {
|
|
690
628
|
return {
|
|
691
629
|
thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
|
|
692
|
-
// report_type
|
|
693
|
-
//
|
|
630
|
+
// report_type enum is undocumented: quarter works; annual/half return 5003 (no such data, not a
|
|
631
|
+
// bad parameter), so do not whitelist.
|
|
694
632
|
report_type: text(requiredText(params.report_type, 'report_type', 32), 'report_type'),
|
|
695
633
|
end_date: dateString(requiredText(params.end_date, 'end_date', 32), 'end_date'),
|
|
696
634
|
};
|
|
@@ -713,7 +651,6 @@ function normalizeFundManager(params) {
|
|
|
713
651
|
function normalizeFundCompany(params) {
|
|
714
652
|
return { company_id: identifierToken(params.company_id, 'company_id') };
|
|
715
653
|
}
|
|
716
|
-
/** 报告期查询(bond-history 与 stock-history 同构)。 */
|
|
717
654
|
function normalizeFundReportType(params) {
|
|
718
655
|
const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
|
|
719
656
|
if (params.report_type !== undefined)
|
|
@@ -736,7 +673,6 @@ function normalizeManagerRange(params) {
|
|
|
736
673
|
range: requiredEnum(params.range, 'range', MANAGER_RANGES, lowerCase),
|
|
737
674
|
};
|
|
738
675
|
}
|
|
739
|
-
/** 指标画线:indexes 为分组数组,time_range 为时间范围对象(都是 JSON 字符串)。 */
|
|
740
676
|
function normalizeFundIndicatorLine(params) {
|
|
741
677
|
const indexes = jsonParam(params.indexes, 'indexes', 'array');
|
|
742
678
|
const groups = JSON.parse(indexes);
|
|
@@ -825,7 +761,6 @@ function normalizeLimitDownPool(params) {
|
|
|
825
761
|
function normalizeLimitBreakPool(params) {
|
|
826
762
|
return normalizePoolQuery(params, LIMIT_BREAK_SORT_FIELDS);
|
|
827
763
|
}
|
|
828
|
-
/** 飙升榜 / 热股榜:period 取 day(日/24 小时级)或 hour(小时级)。 */
|
|
829
764
|
function normalizeRankList(params) {
|
|
830
765
|
const normalized = {};
|
|
831
766
|
if (params.period !== undefined)
|
|
@@ -846,7 +781,7 @@ function normalizeHotStockRankTrend(params) {
|
|
|
846
781
|
end_date: endDate,
|
|
847
782
|
};
|
|
848
783
|
}
|
|
849
|
-
/**
|
|
784
|
+
/** Empty or unknown tags return 1002. */
|
|
850
785
|
function anomalyTagList(value, name) {
|
|
851
786
|
const raw = requiredText(value, name, MAX_TEXT_LENGTH);
|
|
852
787
|
const tokens = raw.split(',').map((token) => token.trim().toUpperCase());
|
|
@@ -962,7 +897,6 @@ function endpointDefinitions() {
|
|
|
962
897
|
normalize: () => ({}),
|
|
963
898
|
guard: { item: { date_ms: 'number', date: 'text' } },
|
|
964
899
|
},
|
|
965
|
-
// ── Phase 2:代码表与除复权事件 ──────────────────────────────────────────
|
|
966
900
|
{
|
|
967
901
|
name: 'get_meta_tickers_list',
|
|
968
902
|
capability: 'ticker_list',
|
|
@@ -1007,7 +941,6 @@ function endpointDefinitions() {
|
|
|
1007
941
|
item: { ticker: 'text?', ex_date_ms: 'number', dividend_per_share: 'number?', per_share_bonus: 'number?' },
|
|
1008
942
|
},
|
|
1009
943
|
},
|
|
1010
|
-
// ── Phase 2:财务三表与财务指标 ──────────────────────────────────────────
|
|
1011
944
|
{
|
|
1012
945
|
name: 'get_a_share_financials_income_statements',
|
|
1013
946
|
capability: 'income_statement',
|
|
@@ -1098,7 +1031,6 @@ function endpointDefinitions() {
|
|
|
1098
1031
|
normalize: normalizeFinancialIndicators,
|
|
1099
1032
|
guard: { data: { thscode: 'text', report: 'text' }, custom: validateAbilities },
|
|
1100
1033
|
},
|
|
1101
|
-
// ── Phase 2:估值与集合竞价 ─────────────────────────────────────────────
|
|
1102
1034
|
{
|
|
1103
1035
|
name: 'get_a_share_valuations_snapshot',
|
|
1104
1036
|
capability: 'valuation',
|
|
@@ -1130,7 +1062,6 @@ function endpointDefinitions() {
|
|
|
1130
1062
|
normalize: normalizeAuction,
|
|
1131
1063
|
guard: { data: { auction_phase: 'text?', data_status: 'text?' }, item: auctionFields },
|
|
1132
1064
|
},
|
|
1133
|
-
// ── Phase 2:盘面特色数据 ───────────────────────────────────────────────
|
|
1134
1065
|
{
|
|
1135
1066
|
name: 'get_a_share_special_data_limit_up_pool',
|
|
1136
1067
|
capability: 'limit_up_pool',
|
|
@@ -1177,7 +1108,6 @@ function endpointDefinitions() {
|
|
|
1177
1108
|
normalize: () => ({}),
|
|
1178
1109
|
guard: { data: { window: 'object' }, item: ladderFields },
|
|
1179
1110
|
},
|
|
1180
|
-
// ── Phase 2:指数 ───────────────────────────────────────────────────────
|
|
1181
1111
|
{
|
|
1182
1112
|
name: 'get_a_share_index_catalog_ths_index_list',
|
|
1183
1113
|
capability: 'index_catalog',
|
|
@@ -1238,7 +1168,6 @@ function endpointDefinitions() {
|
|
|
1238
1168
|
normalize: normalizeIndexHistory,
|
|
1239
1169
|
guard: { item: indexHistoryFields },
|
|
1240
1170
|
},
|
|
1241
|
-
// ── Phase 3:盘面特色数据(doc 详情段 + 真实探针确认可用)────────────────
|
|
1242
1171
|
{
|
|
1243
1172
|
name: 'get_a_share_special_data_limit_down_pool',
|
|
1244
1173
|
capability: 'limit_down_pool',
|
|
@@ -1414,7 +1343,6 @@ function endpointDefinitions() {
|
|
|
1414
1343
|
item: dragonTigerStockFields,
|
|
1415
1344
|
},
|
|
1416
1345
|
},
|
|
1417
|
-
// ── Phase 3:短线风向标竞价基准 ─────────────────────────────────────────
|
|
1418
1346
|
{
|
|
1419
1347
|
name: 'get_a_share_auction_short_term_benchmark',
|
|
1420
1348
|
capability: 'auction_benchmark',
|
|
@@ -1434,11 +1362,7 @@ function endpointDefinitions() {
|
|
|
1434
1362
|
},
|
|
1435
1363
|
guard: { data: { date: 'text', date_ms: 'number' }, item: auctionBenchmarkFields },
|
|
1436
1364
|
},
|
|
1437
|
-
//
|
|
1438
|
-
// 定位规则:这些端点全部只吃**单数 thscode**(基金代码带 .OF/.SH/.SZ 后缀),
|
|
1439
|
-
// 经理与公司分别用 manager_id / company_id;ths_api.md 里的 fund_type(otc/exchange/reits)
|
|
1440
|
-
// 实测并不存在(上游会忽略未知参数,我们不忽略)。金额/份额单位多处文档未给出,
|
|
1441
|
-
// 一律按原值转述、不擅自换算。
|
|
1365
|
+
// Fund endpoints take a singular thscode; `fund_type` does not exist; amount units are undocumented.
|
|
1442
1366
|
{
|
|
1443
1367
|
name: 'get_fund_profile_detail',
|
|
1444
1368
|
capability: 'fund_profile',
|
|
@@ -1659,9 +1583,8 @@ function endpointDefinitions() {
|
|
|
1659
1583
|
normalize: normalizeFundCompany,
|
|
1660
1584
|
guard: { item: fundCompanyFields },
|
|
1661
1585
|
},
|
|
1662
|
-
//
|
|
1663
|
-
//
|
|
1664
|
-
// 一律按原值转述;多处百分比为"百分数原值",但回测 metrics 口径文档自相矛盾。
|
|
1586
|
+
// Fund amount/currency units are undocumented (A-share statements say 原币元, funds do not):
|
|
1587
|
+
// report raw values; percentages are raw but the backtest metrics docs contradict themselves.
|
|
1665
1588
|
{
|
|
1666
1589
|
name: 'get_fund_performance_indicators_historical',
|
|
1667
1590
|
capability: 'fund_performance_history',
|
|
@@ -2008,20 +1931,18 @@ function createSource(definition, baseUrl, resolveApiKey) {
|
|
|
2008
1931
|
time_contract: getDataTimeContract(capability),
|
|
2009
1932
|
name,
|
|
2010
1933
|
source: 'api:fuyao',
|
|
2011
|
-
// 内部数据源身份证(provider.kind.resource):宿主路由/审计用,不进入模型协议。
|
|
2012
1934
|
data_key: buildDataKey('fuyao', 'api', path),
|
|
2013
1935
|
source_label: 'fuyao',
|
|
2014
1936
|
paginated: definition.paginated === true,
|
|
2015
1937
|
cacheMaxAgeMs: definition.cacheMaxAgeMs,
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
1938
|
+
// The endpoint declares its row array; dragon-tiger rows are under `stock_items`, otherwise
|
|
1939
|
+
// storage guesses `item` and sees no row collection.
|
|
2018
1940
|
rowShape: { rowKey: definition.guard.itemKey ?? 'item', ...(definition.guard.rowKeys === undefined ? {} : { rowKeys: definition.guard.rowKeys }) },
|
|
2019
1941
|
summary,
|
|
2020
1942
|
description,
|
|
2021
1943
|
input_schema: inputSchema,
|
|
2022
1944
|
output_schema: outputSchema,
|
|
2023
1945
|
};
|
|
2024
|
-
/** 幂等:Hub 入队前调用一次(digest 口径),execute 再调用一次(直连调用也要生效)。 */
|
|
2025
1946
|
const normalize = (params) => {
|
|
2026
1947
|
const values = paramsObject(params);
|
|
2027
1948
|
assertKnownParameters(values, allowed);
|