@v587d/capital-generation 0.0.0-stage → 2.6.1

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +814 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -2
  4. package/assets/cg-icon.png +0 -0
  5. package/capital-config/client.js +28 -0
  6. package/capital-config/client.src.cjs +388 -0
  7. package/capital-config/index.js +113 -0
  8. package/capital-config/package.json +36 -0
  9. package/capital-watchlist/client.js +182 -0
  10. package/capital-watchlist/client.src.cjs +1229 -0
  11. package/capital-watchlist/index.js +772 -0
  12. package/capital-watchlist/package.json +37 -0
  13. package/chart-ui/README.md +112 -0
  14. package/chart-ui/client.js +16 -0
  15. package/chart-ui/client.src.cjs +126 -0
  16. package/chart-ui/index.js +205 -0
  17. package/chart-ui/package.json +30 -0
  18. package/cordis.patch.yml +72 -0
  19. package/lib/agents/bash-guard.d.ts +55 -0
  20. package/lib/agents/bash-guard.js +162 -0
  21. package/lib/agents/root-tool-policy.d.ts +164 -0
  22. package/lib/agents/root-tool-policy.js +309 -0
  23. package/lib/chart/artifact-ref.d.ts +65 -0
  24. package/lib/chart/artifact-ref.js +107 -0
  25. package/lib/chart/downsample.d.ts +8 -0
  26. package/lib/chart/downsample.js +53 -0
  27. package/lib/chart/errors.d.ts +16 -0
  28. package/lib/chart/errors.js +15 -0
  29. package/lib/chart/events.d.ts +120 -0
  30. package/lib/chart/events.js +471 -0
  31. package/lib/chart/html.d.ts +21 -0
  32. package/lib/chart/html.js +154 -0
  33. package/lib/chart/runtime.d.ts +20 -0
  34. package/lib/chart/runtime.js +270 -0
  35. package/lib/chart/series.d.ts +102 -0
  36. package/lib/chart/series.js +424 -0
  37. package/lib/chart/source-token.d.ts +64 -0
  38. package/lib/chart/source-token.js +217 -0
  39. package/lib/chart/spec.d.ts +58 -0
  40. package/lib/chart/spec.js +265 -0
  41. package/lib/chart/tool.d.ts +86 -0
  42. package/lib/chart/tool.js +288 -0
  43. package/lib/chart/vendor/LICENSE +201 -0
  44. package/lib/chart/vendor/VERSION.json +8 -0
  45. package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
  46. package/lib/data-collector/args.d.ts +38 -0
  47. package/lib/data-collector/args.js +114 -0
  48. package/lib/data-collector/dataset-tools.d.ts +3 -0
  49. package/lib/data-collector/dataset-tools.js +421 -0
  50. package/lib/data-collector/describe.d.ts +99 -0
  51. package/lib/data-collector/describe.js +256 -0
  52. package/lib/data-collector/hub.d.ts +187 -0
  53. package/lib/data-collector/hub.js +469 -0
  54. package/lib/data-collector/query-time.d.ts +58 -0
  55. package/lib/data-collector/query-time.js +101 -0
  56. package/lib/data-collector/query.d.ts +57 -0
  57. package/lib/data-collector/query.js +473 -0
  58. package/lib/data-collector/store.d.ts +499 -0
  59. package/lib/data-collector/store.js +1486 -0
  60. package/lib/data-collector/time-axis.d.ts +169 -0
  61. package/lib/data-collector/time-axis.js +419 -0
  62. package/lib/data-collector/tools.d.ts +32 -0
  63. package/lib/data-collector/tools.js +216 -0
  64. package/lib/index.d.ts +275 -0
  65. package/lib/index.js +458 -0
  66. package/lib/net/eastmoney-client.d.ts +104 -0
  67. package/lib/net/eastmoney-client.js +118 -0
  68. package/lib/net/throttle.d.ts +16 -0
  69. package/lib/net/throttle.js +34 -0
  70. package/lib/ocr/client.d.ts +82 -0
  71. package/lib/ocr/client.js +281 -0
  72. package/lib/ocr/markdown.d.ts +75 -0
  73. package/lib/ocr/markdown.js +209 -0
  74. package/lib/ocr/store.d.ts +119 -0
  75. package/lib/ocr/store.js +226 -0
  76. package/lib/sources/eastmoney-http.d.ts +2 -0
  77. package/lib/sources/eastmoney-http.js +1253 -0
  78. package/lib/sources/fuyao-core.d.ts +52 -0
  79. package/lib/sources/fuyao-core.js +96 -0
  80. package/lib/sources/fuyao-rest.d.ts +14 -0
  81. package/lib/sources/fuyao-rest.js +2057 -0
  82. package/lib/sources/security-code.d.ts +27 -0
  83. package/lib/sources/security-code.js +104 -0
  84. package/lib/sources/security-identity.d.ts +24 -0
  85. package/lib/sources/security-identity.js +89 -0
  86. package/lib/sources/tencent-http.d.ts +2 -0
  87. package/lib/sources/tencent-http.js +1053 -0
  88. package/lib/sources/wind-mcp.d.ts +8 -0
  89. package/lib/sources/wind-mcp.js +593 -0
  90. package/lib/time/tools.d.ts +74 -0
  91. package/lib/time/tools.js +464 -0
  92. package/lib/tool-exec.d.ts +29 -0
  93. package/lib/tool-exec.js +17 -0
  94. package/lib/web-retriever/engines.d.ts +61 -0
  95. package/lib/web-retriever/engines.js +202 -0
  96. package/lib/web-retriever/html-markdown.d.ts +50 -0
  97. package/lib/web-retriever/html-markdown.js +321 -0
  98. package/lib/web-retriever/local-fetch.d.ts +121 -0
  99. package/lib/web-retriever/local-fetch.js +562 -0
  100. package/lib/web-retriever/retriever.d.ts +58 -0
  101. package/lib/web-retriever/retriever.js +94 -0
  102. package/lib/web-retriever/sources.d.ts +235 -0
  103. package/lib/web-retriever/sources.js +961 -0
  104. package/lib/web-retriever/tools.d.ts +50 -0
  105. package/lib/web-retriever/tools.js +1029 -0
  106. package/lib/web-retriever/wind-client.d.ts +69 -0
  107. package/lib/web-retriever/wind-client.js +311 -0
  108. package/locale/en.json +6 -0
  109. package/locale/zh.json +6 -0
  110. package/package.json +84 -4
  111. package/preset/capital-generation/README.md +161 -0
  112. package/preset/capital-generation/agent.patch.yml +509 -0
  113. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
  114. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
  115. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
  116. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
  117. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
@@ -0,0 +1,2057 @@
1
+ import { buildDataKey } from '../data-collector/hub.js';
2
+ import { getDataTimeContract } from '../time/tools.js';
3
+ import { DEFAULT_FUYAO_BASE_URL, FUYAO_MAX_PARAM_LENGTH, fuyaoQuery, fuyaoRequest } from './fuyao-core.js';
4
+ const DEFAULT_BASE_URL = DEFAULT_FUYAO_BASE_URL;
5
+ /**
6
+ * 参数规范化与校验的契约:
7
+ *
8
+ * - `normalize` 同时完成「未知参数拦截 → 必填检查 → 语义校验 → 规范化」四件事,
9
+ * 并且必须幂等:Hub 会在入队前调用一次(用于 merge key 与 params_digest),
10
+ * execute 再调用一次(直接调用 source 时也要生效)。
11
+ * - 规范化在 digest 之前发生,保证「语义相同、书写不同」的请求(大小写、空白、
12
+ * 逗号列表中的重复项、可忽略的默认值)命中同一个 Dataset,而不是各写一份。
13
+ * - 所有错误都是本地错误:不发请求、不落盘、不做网络层猜测。
14
+ * - **不注入上游默认值**:省略参数与显式传默认值是两种请求形态,各自的 digest 稳定;
15
+ * 把上游默认值写进本地代码,会让 digest 的含义随上游默认值漂移而悄悄改变。
16
+ */
17
+ /** A 股完整代码:六位数字 + 交易所后缀。裸代码会被拒绝(上游禁止自行拼接后缀)。 */
18
+ const A_SHARE_CODE = /^\d{6}\.(SH|SZ|BJ)$/;
19
+ /** 指数代码:交易所指数(`.SH`/`.SZ`)与同花顺指数(`.TI`)。 */
20
+ const INDEX_CODE = /^\d{6}\.(SH|SZ|BJ|TI)$/;
21
+ const EXCHANGES = ['SH', 'SZ', 'BJ'];
22
+ /**
23
+ * ticker_search 的交易所与资产类型**按官方契约全量放开**(含期货/期权交易所与 futures/options)。
24
+ * 注意这是元数据域:本 preset 主动不接入期货/期权的**数据**端点,但"能否消歧一个代码"与
25
+ * "能否取到它的行情"是两件事——本地收窄枚举只会让上游支持的查询在这里莫名失败。
26
+ */
27
+ const SEARCH_EXCHANGES = ['SH', 'SZ', 'BJ', 'SSE', 'SZSE', 'CFFEX', 'SHFE', 'INE', 'DCE', 'CZCE', 'GFEX'];
28
+ const SEARCH_ASSET_TYPES = ['a-share', 'a-share-index', 'forex', 'fund-otc', 'fund-etf', 'fund-lof', 'fund-reits', 'futures', 'options'];
29
+ const CATALOG_ASSET_TYPES = ['a-share', 'a-share-index', 'fund-otc', 'fund-etf', 'fund-lof', 'fund-reits', 'forex', 'futures', 'options'];
30
+ const KLINE_INTERVALS = ['1d'];
31
+ const ADJUST_MODES = ['none', 'forward', 'backward'];
32
+ const REPORT_PERIODS = ['annual', 'quarterly'];
33
+ const AUCTION_STAGES = ['live', 'final'];
34
+ const SORT_DIRECTIONS = ['asc', 'desc'];
35
+ const LIMIT_UP_SORT_FIELDS = ['last_price', 'continue_day_cnt', 'seal_money', 'limit_up_time'];
36
+ const INDEX_TAGS = ['cn_concept', 'region', 'tszs', 'industry'];
37
+ const RANK_PERIODS = ['day', 'hour'];
38
+ const ANOMALY_TAGS = ['LIMIT_UP', 'LIMIT_DOWN', 'SHARP_RISE', 'SHARP_FALL', 'RAPID_RALLY', 'RAPID_DECLINE'];
39
+ const DRAGON_TIGER_BOARDS = ['all', 'org', 'hot_money'];
40
+ const LIMIT_DOWN_SORT_FIELDS = ['last_limit_time', 'first_limit_time', 'last_price', 'price_change_ratio_pct', 'turnover_ratio_pct'];
41
+ const LIMIT_BREAK_SORT_FIELDS = ['price_change_ratio_pct', 'open_times', 'last_price', 'turnover_ratio_pct', 'turnover'];
42
+ /** 个股异动批量查询的原始 token 上限(按文档:去重前校验)。 */
43
+ const MAX_ANOMALY_CODES = 50;
44
+ /** 基金代码:六位数字 + 市场后缀(.OF 场外 / .SH / .SZ)。 */
45
+ const FUND_CODE = /^\d{6}\.(OF|SH|SZ)$/;
46
+ const NAV_RANGES = ['week', 'month', 'tmonth', 'hyear', 'year', 'twoyear', 'tyear', 'fyear'];
47
+ const NAV_TYPES = ['unit', 'adj', 'unit,adj'];
48
+ const MERGE_SCOPES = ['all', 'merged', 'separate'];
49
+ /** 基金场内历史 K 线窗口上限:上游规定最长 5 个自然年(不是 A 股的 10 年)。 */
50
+ const MAX_FUND_HISTORY_WINDOW_MS = 1830 * 24 * 60 * 60 * 1000;
51
+ /** 基金前十大持有人的 limit 上限(实测 0 / 11 / 201 均返回 1003)。 */
52
+ const MAX_TOP_HOLDERS_LIMIT = 10;
53
+ /** 基金经理区间收益的 range 枚举(与 nav 的 8 值枚举不同,只有 5 个)。 */
54
+ const MANAGER_RANGES = ['month', 'tmonth', 'year', 'nowyear', 'now'];
55
+ const OFFERING_SUBSCRIBE = ['active', 'upcoming'];
56
+ /** 单值参数长度上限;与 `fuyao-core` 的查询串硬上限同值(同一份知识)。 */
57
+ const MAX_PARAM_LENGTH = FUYAO_MAX_PARAM_LENGTH;
58
+ const MAX_TEXT_LENGTH = 512;
59
+ /** 批量代码接口的原始 token 上限(按文档:去重前校验)。 */
60
+ const MAX_BATCH_CODES = 100;
61
+ /**
62
+ * 历史 K 线 / 财务时间区间上限:上游规定 end - start 不超过 10 年(超出返回 1003)。
63
+ * 本地用 3660 天(含闰日冗余)保守前置拦截,只挡明显越界,不与上游口径冲突。
64
+ */
65
+ const MAX_HISTORY_WINDOW_MS = 3660 * 24 * 60 * 60 * 1000;
66
+ /** 递归按键排序的规范化 JSON:语义相同、键序不同的 JSON 参数会归一到同一个 digest。 */
67
+ function canonicalJson(value) {
68
+ if (value === null || typeof value !== 'object')
69
+ return JSON.stringify(value);
70
+ if (Array.isArray(value))
71
+ return `[${value.map(canonicalJson).join(',')}]`;
72
+ const record = value;
73
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
74
+ }
75
+ /**
76
+ * 需要 URL 编码 JSON 传参的端点(backtest 条件、indicators 选择器、quota 分类)。
77
+ * 校验只做"能解析 + 顶层形状正确 + 长度受限",不臆造上游的内部字段白名单;
78
+ * 规范化后重新序列化,保证等价 JSON 命中同一 Dataset。
79
+ */
80
+ function jsonParam(value, name, kind, maxLength = MAX_PARAM_LENGTH) {
81
+ const raw = requiredText(value, name, maxLength);
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(raw);
85
+ }
86
+ catch {
87
+ fail(`parameter ${name} must be a JSON ${kind} string (got unparsable JSON)`);
88
+ }
89
+ const ok = kind === 'array' ? Array.isArray(parsed) : kind === 'object' ? isRecord(parsed) : (Array.isArray(parsed) || isRecord(parsed));
90
+ if (!ok)
91
+ fail(`parameter ${name} must be a JSON ${kind} string`);
92
+ const encoded = canonicalJson(parsed);
93
+ if (encoded.length > maxLength)
94
+ fail(`parameter ${name} exceeds maximum length ${maxLength}`);
95
+ return encoded;
96
+ }
97
+ function optionalJsonParam(value, name, kind) {
98
+ if (value === undefined)
99
+ return undefined;
100
+ return jsonParam(value, name, kind);
101
+ }
102
+ /** 小写枚举的统一规范化:`LIVE` 与 `live` 是同一个请求。 */
103
+ const lowerCase = (input) => input.toLowerCase();
104
+ function fail(message) {
105
+ throw new Error(message);
106
+ }
107
+ function paramsObject(value) {
108
+ if (!value || typeof value !== 'object' || Array.isArray(value))
109
+ fail('params must be an object');
110
+ return value;
111
+ }
112
+ function isRecord(value) {
113
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
114
+ }
115
+ /** 未知参数一律拒绝:normalize 会重建参数对象,放行等于静默丢弃模型的拼写错误。 */
116
+ function assertKnownParameters(values, allowed) {
117
+ for (const name of Object.keys(values)) {
118
+ if (!allowed.includes(name))
119
+ fail(`unsupported parameter: ${name}`);
120
+ }
121
+ }
122
+ function requiredText(value, name, maxLength = MAX_TEXT_LENGTH) {
123
+ if (value === undefined || value === null || value === '')
124
+ fail(`missing required parameter: ${name}`);
125
+ return text(value, name, maxLength);
126
+ }
127
+ function text(value, name, maxLength = MAX_TEXT_LENGTH) {
128
+ if (typeof value !== 'string')
129
+ fail(`parameter ${name} must be a string`);
130
+ const trimmed = value.trim();
131
+ if (trimmed.length === 0)
132
+ fail(`parameter ${name} must not be empty`);
133
+ if (trimmed.length > maxLength)
134
+ fail(`parameter ${name} exceeds maximum length ${maxLength}`);
135
+ return trimmed;
136
+ }
137
+ function integer(value, name, options = {}) {
138
+ if (typeof value !== 'number' || !Number.isSafeInteger(value))
139
+ fail(`parameter ${name} must be an integer`);
140
+ if (options.minimum !== undefined && value < options.minimum)
141
+ fail(`parameter ${name} must be >= ${options.minimum}`);
142
+ if (options.maximum !== undefined && value > options.maximum)
143
+ fail(`parameter ${name} must be <= ${options.maximum}`);
144
+ return value;
145
+ }
146
+ function requiredInteger(value, name, options = {}) {
147
+ if (value === undefined || value === null)
148
+ fail(`missing required parameter: ${name}`);
149
+ return integer(value, name, options);
150
+ }
151
+ function numberValue(value, name, options = {}) {
152
+ if (typeof value !== 'number' || !Number.isFinite(value))
153
+ fail(`parameter ${name} must be a number`);
154
+ if (options.minimum !== undefined && value < options.minimum)
155
+ fail(`parameter ${name} must be >= ${options.minimum}`);
156
+ return value;
157
+ }
158
+ function booleanValue(value, name) {
159
+ if (typeof value !== 'boolean')
160
+ fail(`parameter ${name} must be a boolean`);
161
+ return value;
162
+ }
163
+ function enumValue(value, name, allowed, normalize = (input) => input) {
164
+ const raw = normalize(text(value, name, 64));
165
+ if (!allowed.includes(raw))
166
+ fail(`parameter ${name} has an invalid value: ${raw} (allowed: ${allowed.join(', ')})`);
167
+ return raw;
168
+ }
169
+ function requiredEnum(value, name, allowed, normalize) {
170
+ if (value === undefined || value === null || value === '')
171
+ fail(`missing required parameter: ${name}`);
172
+ return enumValue(value, name, allowed, normalize);
173
+ }
174
+ /** `YYYY-MM-DD` 日期(企业行为事件的 from/to):格式与日历有效性都校验。 */
175
+ function dateString(value, name) {
176
+ const raw = text(value, name, 32);
177
+ const matched = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
178
+ if (!matched)
179
+ fail(`parameter ${name} must be a date in YYYY-MM-DD form`);
180
+ const [, year, month, day] = matched;
181
+ const monthNumber = Number(month);
182
+ const dayNumber = Number(day);
183
+ if (monthNumber < 1 || monthNumber > 12 || dayNumber < 1 || dayNumber > 31) {
184
+ fail(`parameter ${name} must be a valid calendar date in YYYY-MM-DD form`);
185
+ }
186
+ const parsed = new Date(`${raw}T00:00:00Z`);
187
+ if (Number.isNaN(parsed.getTime()) || !parsed.toISOString().startsWith(raw)) {
188
+ fail(`parameter ${name} must be a valid calendar date in YYYY-MM-DD form`);
189
+ }
190
+ if (Number(year) < 1990)
191
+ fail(`parameter ${name} is outside the supported date range`);
192
+ return raw;
193
+ }
194
+ /** 财务指标报告期:`yyyy-1`..`yyyy-4`(1 一季报 / 2 中报 / 3 三季报 / 4 年报)。 */
195
+ function reportPeriod(value, name) {
196
+ const raw = text(value, name, 16);
197
+ const matched = /^(\d{4})-([1-4])$/.exec(raw);
198
+ if (!matched)
199
+ fail(`parameter ${name} must be a report period like 2025-1 (yyyy-1 .. yyyy-4)`);
200
+ if (Number(matched[1]) < 1990)
201
+ fail(`parameter ${name} is outside the supported period range`);
202
+ return raw;
203
+ }
204
+ /** 规范化单个 A 股代码:trim + 大写 + 后缀校验。 */
205
+ function codeToken(value, name) {
206
+ const raw = text(value, name, 32).toUpperCase();
207
+ if (!A_SHARE_CODE.test(raw)) {
208
+ fail(`parameter ${name} must be a full thscode like 600519.SH; the exchange suffix is required and must not be guessed`);
209
+ }
210
+ return raw;
211
+ }
212
+ /** 规范化单个指数代码(允许同花顺 `.TI`)。 */
213
+ function indexCodeToken(value, name) {
214
+ const raw = text(value, name, 32).toUpperCase();
215
+ if (!INDEX_CODE.test(raw)) {
216
+ fail(`parameter ${name} must be a full index thscode like 000300.SH, 399001.SZ or 886042.TI`);
217
+ }
218
+ return raw;
219
+ }
220
+ /**
221
+ * 规范化逗号分隔代码列表:去空白、去重、保序;空串与非法代码都拒绝。
222
+ * `maximum` 按文档口径在**去重前**校验原始 token 数。
223
+ */
224
+ function codeList(value, name, options = {}) {
225
+ const raw = requiredText(value, name, MAX_PARAM_LENGTH);
226
+ const tokens = raw.split(',').map((token) => token.trim()).filter((token) => token.length > 0);
227
+ if (tokens.length === 0)
228
+ fail(`parameter ${name} must contain at least one thscode`);
229
+ if (options.maximum !== undefined && tokens.length > options.maximum) {
230
+ fail(`parameter ${name} accepts at most ${options.maximum} codes per request`);
231
+ }
232
+ const normalizeToken = options.kind === 'index' ? indexCodeToken : codeToken;
233
+ const unique = [];
234
+ for (const token of tokens) {
235
+ const normalized = normalizeToken(token, name);
236
+ if (!unique.includes(normalized))
237
+ unique.push(normalized);
238
+ }
239
+ return unique.join(',');
240
+ }
241
+ /** 逗号分隔的枚举列表:小写、去重、保序;任一非法值整体拒绝。 */
242
+ function enumList(value, name, allowed) {
243
+ const raw = requiredText(value, name, MAX_TEXT_LENGTH);
244
+ const unique = [];
245
+ for (const token of raw.split(',').map((item) => item.trim().toLowerCase()).filter((item) => item.length > 0)) {
246
+ if (!allowed.includes(token))
247
+ fail(`parameter ${name} has an invalid value: ${token} (allowed: ${allowed.join(', ')})`);
248
+ if (!unique.includes(token))
249
+ unique.push(token);
250
+ }
251
+ if (unique.length === 0)
252
+ fail(`parameter ${name} must contain at least one value`);
253
+ return unique.join(',');
254
+ }
255
+ function objectSchema(properties, required = [], description) {
256
+ return { type: 'object', properties, required, additionalProperties: false, ...(description ? { description } : {}) };
257
+ }
258
+ /**
259
+ * 能力执行侧的信封调用:请求与 `code` 判定住在 `fuyao-core.ts`(host 平面的自选股
260
+ * 与这里共用一份),本层只把它恢复成 Hub 期望的 `{ data, schema }` 形态。
261
+ * 错误文案逐字保持历史形态(2004 / 4001 / 1002 那几条都会透到 Agent 眼前)。
262
+ */
263
+ async function callFuyao(baseUrl, apiKey, path, params, signal, outputSchema) {
264
+ const envelope = await fuyaoRequest({ baseUrl, apiKey, path, search: params, signal });
265
+ return { data: envelope.data ?? null, schema: outputSchema };
266
+ }
267
+ function matchesField(value, spec) {
268
+ const optional = spec.endsWith('?');
269
+ const base = (optional ? spec.slice(0, -1) : spec);
270
+ if (value === undefined)
271
+ return optional;
272
+ if (value === null)
273
+ return true;
274
+ switch (base) {
275
+ case 'text':
276
+ return typeof value === 'string';
277
+ case 'number':
278
+ if (typeof value === 'number')
279
+ return Number.isFinite(value);
280
+ return typeof value === 'string' && value.trim().length > 0 && Number.isFinite(Number(value));
281
+ case 'boolean':
282
+ return typeof value === 'boolean';
283
+ case 'object':
284
+ return isRecord(value);
285
+ case 'array':
286
+ return Array.isArray(value);
287
+ case 'json':
288
+ // "保留上游结构"的字段:文档可能写 object、实际返回 array 或 null
289
+ // (实测基金诊断的 dimensions/resilience 就是数组)。只拒绝 undefined。
290
+ return true;
291
+ }
292
+ }
293
+ function matchesFields(record, fields) {
294
+ return Object.keys(fields).every((name) => matchesField(record[name], fields[name]));
295
+ }
296
+ /** envelope 元数据:字段可缺席或为 null;出现时必须是可用的整数。 */
297
+ function optionalInteger(value) {
298
+ if (value === undefined || value === null)
299
+ return true;
300
+ if (typeof value === 'number')
301
+ return Number.isSafeInteger(value);
302
+ if (typeof value === 'string')
303
+ return Number.isSafeInteger(Number(value));
304
+ return false;
305
+ }
306
+ /** 输出护栏:只拒绝结构不可用的响应,不做业务推断。 */
307
+ function buildGuard(spec) {
308
+ return (data) => {
309
+ if (spec.dataArray) {
310
+ if (!Array.isArray(data))
311
+ return false;
312
+ return data.every((row) => isRecord(row) && matchesFields(row, spec.dataArray));
313
+ }
314
+ // custom-only:顶层可能是数组(quota/list、indicators/line、indicators/table),
315
+ // 由校验器自己判断形状,不能在这里先做 isRecord 拦截。
316
+ if (spec.custom && !spec.data && !spec.item)
317
+ return spec.custom(data);
318
+ if (!isRecord(data))
319
+ return false;
320
+ if (!optionalInteger(data.timestamp) || !optionalInteger(data.total))
321
+ return false;
322
+ if (spec.data && !matchesFields(data, spec.data))
323
+ return false;
324
+ if (spec.custom && !spec.custom(data))
325
+ return false;
326
+ if (spec.item) {
327
+ const rows = data[spec.itemKey ?? 'item'];
328
+ if (!Array.isArray(rows))
329
+ return false;
330
+ if (!rows.every((row) => isRecord(row) && matchesFields(row, spec.item)))
331
+ return false;
332
+ }
333
+ return true;
334
+ };
335
+ }
336
+ /** 财务指标:data.abilities[].indicators[],value 是原始数值字符串或 null(不转 number,保留精度)。 */
337
+ function validateAbilities(data) {
338
+ if (!isRecord(data))
339
+ return false;
340
+ if (!Array.isArray(data.abilities))
341
+ return false;
342
+ return data.abilities.every((ability) => {
343
+ if (!isRecord(ability))
344
+ return false;
345
+ if (typeof ability.ability !== 'string' || !Array.isArray(ability.indicators))
346
+ return false;
347
+ return ability.indicators.every((indicator) => isRecord(indicator)
348
+ && typeof indicator.index_id === 'string'
349
+ && (indicator.value === null || typeof indicator.value === 'string'));
350
+ });
351
+ }
352
+ // timestamp 与 total 在无有效数据时可以是 null(文档明确,如基金行情快照);运行时护栏一直放行 null,
353
+ // 模型可见 schema 也必须一致,否则 describe_capability 给出的契约是错的。
354
+ const nullableInteger = { oneOf: [{ type: 'integer' }, { type: 'null' }] };
355
+ const envelopeData = (item) => ({ type: 'object', properties: { timestamp: nullableInteger, total: nullableInteger, item }, additionalProperties: true });
356
+ 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
+ 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
+ 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
+ const calendarOutput = envelopeData({ type: 'array', items: { type: 'object', properties: { date_ms: { type: 'integer' }, date: { type: 'string' } }, additionalProperties: true } });
360
+ /** 行契约 -> 输出 schema 的字段描述(模型可见,只描述字段名与类型)。 */
361
+ function rowSchema(fields) {
362
+ const properties = {};
363
+ for (const [name, spec] of Object.entries(fields)) {
364
+ const base = spec.endsWith('?') ? spec.slice(0, -1) : spec;
365
+ const optional = spec.endsWith('?');
366
+ const jsonType = base === 'number' ? { type: 'number' }
367
+ : base === 'text' ? { type: 'string' }
368
+ : base === 'boolean' ? { type: 'boolean' }
369
+ : base === 'array' ? { type: 'array', items: {} }
370
+ : base === 'json' ? {}
371
+ : { type: 'object', additionalProperties: true };
372
+ properties[name] = optional ? { oneOf: [jsonType, { type: 'null' }] } : jsonType;
373
+ }
374
+ return { type: 'object', properties, additionalProperties: true };
375
+ }
376
+ const tickerFields = { thscode: 'text', ticker: 'text?', name: 'text?', exchange: 'text?', asset_type: 'text?', currency: 'text?', list_date: 'text?', end_date: 'text?', last_trade_date: 'text?', last_delivery_date: 'text?' };
377
+ const valuationFields = { thscode: 'text', ticker: 'text?', name: 'text?', pe_ttm: 'number?', pe_mrq: 'number?', pb_mrq: 'number?', ps_ttm: 'number?', pcf_ttm: 'number?' };
378
+ const auctionFields = { thscode: 'text', ticker: 'text?', name: 'text?', auction_price: 'number?', auction_pct: 'number?', auction_volume: 'number?', auction_amount: 'number?', auction_unmatched: 'number?', auction_turnover_pct: 'number?', auction_yesterday_ratio_pct: 'number?', auction_volume_ratio: 'number?', pre_close_price: 'number?', open_price: 'number?', last_price: 'number?', float_market_cap: 'number?' };
379
+ const limitUpPoolFields = { thscode: 'text', ticker: 'text?', name: 'text?', is_st: 'boolean?', is_new: 'boolean?', last_price: 'number?', price_change_ratio_pct: 'number?', limit_up_time: 'text?', limit_up_reason: 'text?', continue_day_text: 'text?', continue_day_cnt: 'number?', seal_money: 'number?', max_seal_money: 'number?' };
380
+ const ladderFields = { date: 'text', boards: 'object' };
381
+ const indexCatalogFields = { thscode: 'text', name: 'text?' };
382
+ const indexConstituentFields = { thscode: 'text', ticker: 'text?', name: 'text?' };
383
+ 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
+ const indexHistoryFields = { date_ms: 'number', open_price: 'number?', high_price: 'number?', low_price: 'number?', close_price: 'number?', volume: 'number?', turnover: 'number?' };
385
+ /** 财务三表共有的期间标识字段(严格)与币种(宽容)。 */
386
+ 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
+ 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
+ const rankListFields = { thscode: 'text', ticker: 'text?', name: 'text?', rank: 'number?', heat: 'text?', rank_change: 'number?', rank_trend: 'text?' };
390
+ const hotStockHistoryFields = { thscode: 'text', ticker: 'text?', name: 'text?', rank: 'number?' };
391
+ const rankTrendFields = { thscode: 'text', ticker: 'text?', date: 'text', date_ms: 'number?', rank: 'number?' };
392
+ const anomalyFields = { thscode: 'text', stock_name: 'text?', analysis_content: 'text?', keyword_list: 'array?', tag_name: 'text?' };
393
+ const dragonTigerStockFields = { thscode: 'text', ticker: 'text?', name: 'text?', concept_list: 'array?', change: 'number?', net_value: 'number?', net_rate: 'number?', hot_rank: 'number?', buy_value: 'number?', sell_value: 'number?', limit_reason: 'text?', range_days: 'number?', org_net_value: 'number?', org_net_rate: 'number?', org_buy_num: 'number?', org_sell_num: 'number?', amount: 'number?', hot_money_net_value: 'number?', hot_money_net_rate: 'number?', hot_money_item_net_value: 'number?', hot_money_item_net_rate: 'number?' };
394
+ const auctionBenchmarkFields = { thscode: 'text', ticker: 'text?', name: 'text?', auction_pct: 'number?', tags: 'array?' };
395
+ const fundProfileFields = { thscode: 'text', ticker: 'text?', fund_name: 'text?', estab_date: 'number?', company_id: 'text?', mgmt_name: 'text?', manager_name: 'text?', fund_scale: 'number?', unit_nav: 'number?', manager_info: 'array?', trade_rule: 'array?', rate_info: 'array?' };
396
+ const fundQuoteFields = { thscode: 'text', ticker: 'text?', last_price: 'number?', open_price: 'number?', high_price: 'number?', low_price: 'number?', prev_price: 'number?', price_change_ratio_pct: 'number?', price_change: 'number?', price_amplitude_ratio_pct: 'number?', volume: 'number?', turnover: 'number?', turnover_ratio_pct: 'number?' };
397
+ const fundHistoryFields = { date_ms: 'number', open_price: 'number?', high_price: 'number?', low_price: 'number?', close_price: 'number?', volume: 'number?', turnover: 'number?' };
398
+ const fundNavFields = { nav_date: 'number', unit_nav: 'number?', adj_nav: 'number?' };
399
+ /**
400
+ * 收益率行没有主标识字段,且**任何单个 return_* 都可能缺席**:官方示例就只有 8 个
401
+ * (return_month / tmonth / hyear / year / tyear / fyear / nowyear / now,没有 return_week)。
402
+ * 因此全部字段设为可选,另用 validateFundReturns 要求"至少命中一个收益/排名族字段",
403
+ * 既不放行垃圾结构,也不会误杀官方合法响应。
404
+ */
405
+ const fundReturnsFields = {
406
+ return_week: 'number?', return_month: 'number?', return_tmonth: 'number?', return_hyear: 'number?',
407
+ return_year: 'number?', return_twoyear: 'number?', return_tyear: 'number?', return_fyear: 'number?',
408
+ return_nowyear: 'number?', return_now: 'number?',
409
+ peer_average_week: 'number?', peer_average_month: 'number?', peer_average_tmonth: 'number?', peer_average_hyear: 'number?',
410
+ peer_average_year: 'number?', peer_average_twoyear: 'number?', peer_average_tyear: 'number?', peer_average_fyear: 'number?',
411
+ rank_week: 'number?', rank_month: 'number?', rank_tmonth: 'number?', rank_hyear: 'number?',
412
+ rank_year: 'number?', rank_twoyear: 'number?', rank_tyear: 'number?', rank_fyear: 'number?',
413
+ rank_total_week: 'number?', rank_total_month: 'number?', rank_total_tmonth: 'number?', rank_total_hyear: 'number?',
414
+ rank_total_year: 'number?', rank_total_twoyear: 'number?', rank_total_tyear: 'number?', rank_total_fyear: 'number?',
415
+ };
416
+ /** 收益/排名族的字段前缀;一行至少命中一个才算可用,其余情况判契约不符。 */
417
+ const FUND_RETURN_PREFIXES = ['return_', 'peer_average_', 'rank_'];
418
+ function validateFundReturns(data) {
419
+ if (!isRecord(data) || !Array.isArray(data.item))
420
+ return false;
421
+ return data.item.every((row) => isRecord(row)
422
+ && Object.keys(row).some((key) => FUND_RETURN_PREFIXES.some((prefix) => key.startsWith(prefix))));
423
+ }
424
+ const fundDrawdownFields = { thscode: 'text', ticker: 'text?', week: 'number?', month: 'number?', tmonth: 'number?', hyear: 'number?', year: 'number?', twoyear: 'number?', tyear: 'number?', fyear: 'number?', nowyear: 'number?', now: 'number?' };
425
+ const fundHoldingFields = { thscode: 'text', ticker: 'text?', stock_name: 'text?', hold_ratio: 'number?', asset_type: 'text?', position_capital: 'number?', position_count: 'number?', security_market_value_rate_pct: 'number?', period_increase_rate_pct: 'number?', investment_rank: 'number?', start_date_ms: 'number?', end_date_ms: 'number?', publish_date_ms: 'number?', modify_time_ms: 'number?' };
426
+ const fundAssetAllocationFields = { report_date_ms: 'number', stock_ratio_pct: 'number?', bond_ratio_pct: 'number?', deposit_ratio_pct: 'number?', other_ratio_pct: 'number?' };
427
+ const fundIndustryAllocationFields = { report_period: 'text', industry_name: 'text?', ratio_pct: 'number?' };
428
+ const fundStockHistoryFields = { thscode: 'text', ticker: 'text?', name: 'text?', asset_type: 'text?', hold_ratio: 'number?', market_value: 'number?', period_increase_pct: 'number?', rank: 'number?', report_type: 'text?', end_date_ms: 'number?' };
429
+ const fundHolderFields = { merge_scope: 'text', report_date_ms: 'number', ins_position: 'number?', holder_amount: 'number?', avg_holder_share: 'number?', psnl_rate: 'number?', mgmt_staff_hold_rate: 'number?' };
430
+ const fundTopHolderFields = { holder_id: 'text', holder_code: 'text?', holder_name: 'text?', holder_type: 'text?', rank: 'number?', hold_share: 'number?', hold_rate_pct: 'number?', report_date_ms: 'number?', publish_date_ms: 'number?' };
431
+ const fundManagerFields = { manager_id: 'text', manager_name: 'text?', sex: 'text?', degree: 'text?', company_id: 'text?', company_name: 'text?', resume: 'text?', photo_url: 'text?', annual_return_pct: 'number?', maximum_return_pct: 'number?', radar_comparison: 'array?' };
432
+ const fundCompanyFields = { company_id: 'text', company_name: 'text?', company_type: 'text?', established_date_ms: 'number?', fund_count: 'number?', scale: 'number?' };
433
+ const fundPerformanceHistoryFields = { date_ms: 'number', rsi_pct: 'number?', donchian_channel: 'number?', track_index_pe_ttm_five_year_percentile: 'number?' };
434
+ const fundReportDatesFields = { report_type: 'text', report_type_name: 'text?', start_date_ms: 'number?', end_date_ms: 'number?' };
435
+ const fundManagerExperienceFields = { awards: 'json?', heavy_assets: 'json?', investment_history: 'json?' };
436
+ const fundManagerStyleFields = { representative_fund_thscode: 'text?', representative_fund_ticker: 'text?', representative_fund_name: 'text?', investment_idea: 'text?', total_fund_scale: 'number?', industry_preferences: 'json?' };
437
+ const fundManagerPerformanceFields = { date_ms: 'number', manager_return_pct: 'number?', peer_return_pct: 'number?', benchmark_return_pct: 'number?' };
438
+ const fundFinancialDateFields = { start_date_ms: 'number', end_date_ms: 'number', publish_date_ms: 'number?' };
439
+ const fundNumberFields = (names) => Object.fromEntries(names.map((name) => [name, 'number?']));
440
+ const fundIncomeFields = { ...fundFinancialDateFields, ...fundNumberFields(['income', 'investment_income', 'stock_investment_income', 'bond_investment_income', 'fund_investment_income', 'dividend_income', 'interest_income', 'fair_value_income', 'exchange_income', 'other_income', 'total_income', 'fee', 'total_fee', 'manager_reward', 'custodian_fee', 'transaction_cost', 'tax_surcharge', 'total_profit', 'net_profit']) };
441
+ const fundBalanceFields = { ...fundFinancialDateFields, ...fundNumberFields(['total_assets', 'bank_deposit', 'fund_investment', 'stock_investment', 'bond_investment', 'transactional_financial_assets', 'other_assets', 'total_liability', 'other_liability', 'owner_total_equity', 'undistributed_profit', 'liability_and_owner_equity']) };
442
+ const fundFinancialIndicatorFields = { ...fundFinancialDateFields, ...fundNumberFields(['distribution_profit', 'current_profit', 'current_income', 'distribution_share_profit', 'average_nav_profit_margin', 'average_share_current_profit', 'share_nav', 'sum_nav_rate', 'asset_nav', 'sum_share_nav', 'nav_rate']) };
443
+ 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
+ const fundOfferingFields = { thscode: 'text', ticker: 'text?', subscription_start_ms: 'number?', subscription_end_ms: 'number?' };
445
+ const fundQuotaSummaryFields = { name: 'text', unlimited: 'text?', total_limit: 'text?', total: 'text?', buy: 'text?' };
446
+ /** 实测:dimensions/peer_dimensions/resilience/peer_resilience 是**数组**、probabilities/ranges 为 null(文档写 object)。 */
447
+ 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
+ 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
+ function validateQuotaList(data) {
451
+ if (!Array.isArray(data))
452
+ return false;
453
+ return data.every((category) => {
454
+ if (!isRecord(category))
455
+ return false;
456
+ if (typeof category.name !== 'string' || !Array.isArray(category.sub_tab))
457
+ return false;
458
+ return category.sub_tab.every((subTab) => {
459
+ if (!isRecord(subTab))
460
+ return false;
461
+ if (!Array.isArray(subTab.fund_list))
462
+ return false;
463
+ return subTab.fund_list.every((fund) => isRecord(fund)
464
+ && typeof fund.thscode === 'string'
465
+ && matchesField(fund.quota, 'text?')
466
+ && matchesField(fund.year, 'text?'));
467
+ });
468
+ });
469
+ }
470
+ /** indicators/line:time_range 时间轴 + indexes 元信息 + data 序列,三层结构。 */
471
+ function validateIndicatorLine(data) {
472
+ if (!isRecord(data))
473
+ return false;
474
+ if (!Array.isArray(data.time_range))
475
+ return false;
476
+ if (!data.time_range.every((point) => point === null || Number.isSafeInteger(point)))
477
+ return false;
478
+ if (!Array.isArray(data.indexes) || !Array.isArray(data.data))
479
+ return false;
480
+ if (!data.indexes.every((meta) => isRecord(meta) && typeof meta.index_id === 'string'))
481
+ return false;
482
+ return data.data.every((series) => {
483
+ if (!isRecord(series) || typeof series.thscode !== 'string' || !Array.isArray(series.values))
484
+ return false;
485
+ return series.values.every((value) => isRecord(value) && Number.isSafeInteger(value.idx));
486
+ });
487
+ }
488
+ /** indicators/table:total + indexes + data[],值可为 JSON 或 null。 */
489
+ function validateIndicatorTable(data) {
490
+ if (!isRecord(data))
491
+ return false;
492
+ if (!Number.isSafeInteger(data.total))
493
+ return false;
494
+ if (!Array.isArray(data.indexes) || !Array.isArray(data.data))
495
+ return false;
496
+ if (!data.indexes.every((meta) => isRecord(meta) && typeof meta.index_id === 'string'))
497
+ return false;
498
+ return data.data.every((series) => {
499
+ if (!isRecord(series) || typeof series.thscode !== 'string' || !Array.isArray(series.values))
500
+ return false;
501
+ return series.values.every((value) => isRecord(value) && Number.isSafeInteger(value.idx));
502
+ });
503
+ }
504
+ const financialCommonFields = {
505
+ thscode: 'text',
506
+ ticker: 'text?',
507
+ period: 'text',
508
+ fiscal_year: 'number',
509
+ fiscal_period: 'text',
510
+ report_date_ms: 'number',
511
+ period_end_ms: 'number',
512
+ currency: 'text?',
513
+ };
514
+ const financialNumber = (names) => Object.fromEntries(names.map((name) => [name, 'number?']));
515
+ function normalizeQuote(params) {
516
+ const normalized = {};
517
+ if (params.thscodes !== undefined) {
518
+ normalized.thscodes = codeList(params.thscodes, 'thscodes');
519
+ // 按文档:传入 thscodes 时上游忽略分页;这里直接丢弃,避免产生语义等价的多个 Dataset。
520
+ return normalized;
521
+ }
522
+ if (params.limit !== undefined)
523
+ normalized.limit = integer(params.limit, 'limit', { minimum: 1 });
524
+ if (params.offset !== undefined)
525
+ normalized.offset = integer(params.offset, 'offset', { minimum: 0 });
526
+ return normalized;
527
+ }
528
+ /** 时间区间型请求的共用校验:成对出现、end >= start、窗口不超过 10 年。 */
529
+ function timeWindow(params, normalized) {
530
+ const { start, end } = params;
531
+ if ((start === undefined) !== (end === undefined))
532
+ fail('parameters start and end must be provided together');
533
+ if (start === undefined || end === undefined)
534
+ return;
535
+ const from = integer(start, 'start', { minimum: 0 });
536
+ const to = integer(end, 'end', { minimum: 0 });
537
+ if (to < from)
538
+ fail('parameter end must be >= start');
539
+ if (to - from > MAX_HISTORY_WINDOW_MS)
540
+ fail('parameter window (end - start) exceeds the 10 year limit');
541
+ normalized.start = from;
542
+ normalized.end = to;
543
+ }
544
+ function normalizeHistory(params) {
545
+ const normalized = {
546
+ thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
547
+ interval: requiredEnum(params.interval, 'interval', KLINE_INTERVALS, lowerCase),
548
+ };
549
+ const start = requiredInteger(params.start, 'start', { minimum: 0 });
550
+ const end = requiredInteger(params.end, 'end', { minimum: 0 });
551
+ if (end < start)
552
+ fail('parameter end must be >= start');
553
+ if (end - start > MAX_HISTORY_WINDOW_MS)
554
+ fail('parameter window (end - start) exceeds the 10 year limit');
555
+ normalized.start = start;
556
+ normalized.end = end;
557
+ if (params.adjust !== undefined)
558
+ normalized.adjust = enumValue(params.adjust, 'adjust', ADJUST_MODES, lowerCase);
559
+ if (params.offset !== undefined)
560
+ normalized.offset = integer(params.offset, 'offset', { minimum: 0 });
561
+ return normalized;
562
+ }
563
+ function normalizeTickerSearch(params) {
564
+ const normalized = { q: requiredText(params.q, 'q') };
565
+ if (params.exchange !== undefined)
566
+ normalized.exchange = enumValue(params.exchange, 'exchange', SEARCH_EXCHANGES, (input) => input.toUpperCase());
567
+ if (params.asset_type !== undefined)
568
+ normalized.asset_type = enumList(params.asset_type, 'asset_type', SEARCH_ASSET_TYPES);
569
+ if (params.limit !== undefined)
570
+ normalized.limit = integer(params.limit, 'limit', { minimum: 1, maximum: 50 });
571
+ return normalized;
572
+ }
573
+ function normalizeTickerList(params) {
574
+ const normalized = {};
575
+ if (params.asset_type !== undefined)
576
+ normalized.asset_type = enumList(params.asset_type, 'asset_type', CATALOG_ASSET_TYPES);
577
+ if (params.limit !== undefined)
578
+ normalized.limit = integer(params.limit, 'limit', { minimum: 1, maximum: 10000 });
579
+ if (params.offset !== undefined)
580
+ normalized.offset = integer(params.offset, 'offset', { minimum: 0 });
581
+ return normalized;
582
+ }
583
+ function normalizeCorporateActions(params) {
584
+ const normalized = { thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
585
+ const from = params.from === undefined ? undefined : dateString(params.from, 'from');
586
+ const to = params.to === undefined ? undefined : dateString(params.to, 'to');
587
+ if (from !== undefined && to !== undefined && to < from)
588
+ fail('parameter to must be >= from');
589
+ if (from !== undefined)
590
+ normalized.from = from;
591
+ if (to !== undefined)
592
+ normalized.to = to;
593
+ return normalized;
594
+ }
595
+ /** 财务三表共用参数:最近 N 期(limit)与时间区间(start+end)互斥、二选一。 */
596
+ function normalizeFinancial(params) {
597
+ const normalized = {
598
+ thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
599
+ period: requiredEnum(params.period, 'period', REPORT_PERIODS, lowerCase),
600
+ };
601
+ const hasWindow = params.start !== undefined || params.end !== undefined;
602
+ if (params.limit !== undefined && hasWindow) {
603
+ fail('parameter limit is mutually exclusive with start/end: choose the recent-N-periods mode or the time-range mode');
604
+ }
605
+ if (params.limit !== undefined)
606
+ normalized.limit = integer(params.limit, 'limit', { minimum: 1, maximum: 20 });
607
+ timeWindow(params, normalized);
608
+ return normalized;
609
+ }
610
+ function normalizeFinancialIndicators(params) {
611
+ return {
612
+ thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
613
+ report: reportPeriod(requiredText(params.report, 'report', 16), 'report'),
614
+ };
615
+ }
616
+ function normalizeValuation(params) {
617
+ return { thscodes: codeList(params.thscodes, 'thscodes', { maximum: MAX_BATCH_CODES }) };
618
+ }
619
+ function normalizeAuction(params) {
620
+ const normalized = { thscodes: codeList(params.thscodes, 'thscodes', { maximum: MAX_BATCH_CODES }) };
621
+ if (params.stage !== undefined)
622
+ normalized.stage = enumValue(params.stage, 'stage', AUCTION_STAGES, lowerCase);
623
+ return normalized;
624
+ }
625
+ function normalizeLimitUpPool(params) {
626
+ const normalized = {};
627
+ if (params.date_ms !== undefined)
628
+ normalized.date_ms = integer(params.date_ms, 'date_ms', { minimum: 0 });
629
+ if (params.page !== undefined)
630
+ normalized.page = integer(params.page, 'page', { minimum: 1 });
631
+ if (params.size !== undefined)
632
+ normalized.size = integer(params.size, 'size', { minimum: 1, maximum: 200 });
633
+ if (params.sort_field !== undefined)
634
+ normalized.sort_field = enumValue(params.sort_field, 'sort_field', LIMIT_UP_SORT_FIELDS, lowerCase);
635
+ if (params.sort_dir !== undefined)
636
+ normalized.sort_dir = enumValue(params.sort_dir, 'sort_dir', SORT_DIRECTIONS, lowerCase);
637
+ return normalized;
638
+ }
639
+ function normalizeIndexCatalog(params) {
640
+ const normalized = {};
641
+ if (params.tag !== undefined)
642
+ normalized.tag = enumValue(params.tag, 'tag', INDEX_TAGS, lowerCase);
643
+ return normalized;
644
+ }
645
+ function normalizeIndexConstituents(params) {
646
+ return { thscode: indexCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
647
+ }
648
+ function normalizeIndexQuote(params) {
649
+ return { thscodes: codeList(params.thscodes, 'thscodes', { kind: 'index' }) };
650
+ }
651
+ /** 基金代码规范化:trim + 大写 + 市场后缀校验(不接受逗号与裸代码)。 */
652
+ function fundCodeToken(value, name) {
653
+ const raw = text(value, name, 32).toUpperCase();
654
+ if (!FUND_CODE.test(raw)) {
655
+ fail(`parameter ${name} must be a full fund thscode like 025480.OF, 510300.SH or 161725.SZ`);
656
+ }
657
+ return raw;
658
+ }
659
+ /** 基金经理 / 基金公司标识:格式上游未公开,只做非空与长度约束。 */
660
+ function identifierToken(value, name) {
661
+ return requiredText(value, name, 64);
662
+ }
663
+ /** 只带 thscode 的基金端点(基本资料、业绩快照、持仓、持有人等)。 */
664
+ function onlyFundCode(params) {
665
+ return { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
666
+ }
667
+ function normalizeFundMarketHistory(params) {
668
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
669
+ if (params.interval !== undefined)
670
+ normalized.interval = enumValue(params.interval, 'interval', KLINE_INTERVALS, lowerCase);
671
+ const start = requiredInteger(params.start, 'start', { minimum: 0 });
672
+ const end = requiredInteger(params.end, 'end', { minimum: 0 });
673
+ if (end < start)
674
+ fail('parameter end must be >= start');
675
+ if (end - start > MAX_FUND_HISTORY_WINDOW_MS)
676
+ fail('parameter window (end - start) exceeds the 5 year limit for fund market history');
677
+ normalized.start = start;
678
+ normalized.end = end;
679
+ return normalized;
680
+ }
681
+ function normalizeFundNav(params) {
682
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
683
+ if (params.range !== undefined)
684
+ normalized.range = enumValue(params.range, 'range', NAV_RANGES, lowerCase);
685
+ if (params.nav_type !== undefined)
686
+ normalized.nav_type = enumValue(params.nav_type, 'nav_type', NAV_TYPES, lowerCase);
687
+ return normalized;
688
+ }
689
+ function normalizeFundStockHistory(params) {
690
+ return {
691
+ thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
692
+ // report_type 的枚举上游未公开(实测 quarter 可用;annual/half 返回 5003 即上游无该类型数据,
693
+ // 而不是参数错误),因此本地不做白名单,只拒绝空值与明显非法字符,避免误拒上游新增类型。
694
+ report_type: text(requiredText(params.report_type, 'report_type', 32), 'report_type'),
695
+ end_date: dateString(requiredText(params.end_date, 'end_date', 32), 'end_date'),
696
+ };
697
+ }
698
+ function normalizeFundHolders(params) {
699
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
700
+ if (params.merge_scope !== undefined)
701
+ normalized.merge_scope = enumValue(params.merge_scope, 'merge_scope', MERGE_SCOPES, lowerCase);
702
+ return normalized;
703
+ }
704
+ function normalizeFundTopHolders(params) {
705
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
706
+ if (params.limit !== undefined)
707
+ normalized.limit = integer(params.limit, 'limit', { minimum: 1, maximum: MAX_TOP_HOLDERS_LIMIT });
708
+ return normalized;
709
+ }
710
+ function normalizeFundManager(params) {
711
+ return { manager_id: identifierToken(params.manager_id, 'manager_id') };
712
+ }
713
+ function normalizeFundCompany(params) {
714
+ return { company_id: identifierToken(params.company_id, 'company_id') };
715
+ }
716
+ /** 报告期查询(bond-history 与 stock-history 同构)。 */
717
+ function normalizeFundReportType(params) {
718
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
719
+ if (params.report_type !== undefined)
720
+ normalized.report_type = text(params.report_type, 'report_type', 32);
721
+ return normalized;
722
+ }
723
+ function normalizeFundPerformanceHistory(params) {
724
+ const normalized = { thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode') };
725
+ const start = requiredInteger(params.start, 'start', { minimum: 0 });
726
+ const end = requiredInteger(params.end, 'end', { minimum: 0 });
727
+ if (end < start)
728
+ fail('parameter end must be >= start');
729
+ normalized.start = start;
730
+ normalized.end = end;
731
+ return normalized;
732
+ }
733
+ function normalizeManagerRange(params) {
734
+ return {
735
+ manager_id: identifierToken(params.manager_id, 'manager_id'),
736
+ range: requiredEnum(params.range, 'range', MANAGER_RANGES, lowerCase),
737
+ };
738
+ }
739
+ /** 指标画线:indexes 为分组数组,time_range 为时间范围对象(都是 JSON 字符串)。 */
740
+ function normalizeFundIndicatorLine(params) {
741
+ const indexes = jsonParam(params.indexes, 'indexes', 'array');
742
+ const groups = JSON.parse(indexes);
743
+ if (groups.length === 0)
744
+ fail('parameter indexes must contain at least one group');
745
+ for (const [position, group] of groups.entries()) {
746
+ if (!isRecord(group))
747
+ fail(`parameter indexes group ${position} must be an object`);
748
+ if (!Array.isArray(group.thscodes) || group.thscodes.length === 0) {
749
+ fail(`parameter indexes group ${position} must contain a non-empty thscodes array`);
750
+ }
751
+ if (!Array.isArray(group.index_info) || group.index_info.length === 0) {
752
+ fail(`parameter indexes group ${position} must contain a non-empty index_info array`);
753
+ }
754
+ for (const [index, info] of group.index_info.entries()) {
755
+ if (!isRecord(info) || typeof info.index_id !== 'string' || info.index_id.length === 0) {
756
+ fail(`parameter indexes group ${position} index_info[${index}] must contain a string index_id`);
757
+ }
758
+ }
759
+ }
760
+ const timeRange = jsonParam(params.time_range, 'time_range', 'object');
761
+ const range = JSON.parse(timeRange);
762
+ if (typeof range.time_type !== 'string' || range.time_type.length === 0) {
763
+ fail('parameter time_range must contain a string time_type');
764
+ }
765
+ for (const field of ['start', 'end', 'offset']) {
766
+ const value = range[field];
767
+ if (value !== undefined && (typeof value !== 'number' || !Number.isSafeInteger(value))) {
768
+ fail(`parameter time_range.${field} must be an integer when present`);
769
+ }
770
+ }
771
+ return { indexes, time_range: timeRange };
772
+ }
773
+ function normalizeFundIndicatorTable(params) {
774
+ const normalized = {};
775
+ const codeSelectors = optionalJsonParam(params.code_selectors, 'code_selectors', 'object');
776
+ const indexes = optionalJsonParam(params.indexes, 'indexes', 'array');
777
+ const pageInfo = optionalJsonParam(params.page_info, 'page_info', 'object');
778
+ const sort = optionalJsonParam(params.sort, 'sort', 'array');
779
+ if (codeSelectors !== undefined)
780
+ normalized.code_selectors = codeSelectors;
781
+ if (indexes !== undefined)
782
+ normalized.indexes = indexes;
783
+ if (pageInfo !== undefined)
784
+ normalized.page_info = pageInfo;
785
+ if (sort !== undefined)
786
+ normalized.sort = sort;
787
+ if (Object.keys(normalized).length === 0) {
788
+ fail('at least one of code_selectors / indexes / page_info / sort is required (this endpoint has no业务默认值)');
789
+ }
790
+ return normalized;
791
+ }
792
+ function normalizeFundQuotaList(params) {
793
+ const normalized = { tab: jsonParam(params.tab, 'tab', 'array') };
794
+ if (params.buy !== undefined)
795
+ normalized.buy = booleanValue(params.buy, 'buy');
796
+ return normalized;
797
+ }
798
+ function normalizeFundBacktest(params) {
799
+ return {
800
+ thscode: fundCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
801
+ buy_conditions: jsonParam(params.buy_conditions, 'buy_conditions', 'object-or-array'),
802
+ sell_conditions: jsonParam(params.sell_conditions, 'sell_conditions', 'object-or-array'),
803
+ buy_frequency_type: requiredText(params.buy_frequency_type, 'buy_frequency_type', 32),
804
+ max_buy_times: requiredInteger(params.max_buy_times, 'max_buy_times', { minimum: 1 }),
805
+ per_buy_amount: numberValue(params.per_buy_amount, 'per_buy_amount', { minimum: 0 }),
806
+ };
807
+ }
808
+ function normalizePoolQuery(params, sortFields) {
809
+ const normalized = {};
810
+ if (params.date_ms !== undefined)
811
+ normalized.date_ms = integer(params.date_ms, 'date_ms', { minimum: 0 });
812
+ if (params.page !== undefined)
813
+ normalized.page = integer(params.page, 'page', { minimum: 1 });
814
+ if (params.size !== undefined)
815
+ normalized.size = integer(params.size, 'size', { minimum: 1, maximum: 200 });
816
+ if (params.sort_field !== undefined)
817
+ normalized.sort_field = enumValue(params.sort_field, 'sort_field', sortFields, lowerCase);
818
+ if (params.sort_dir !== undefined)
819
+ normalized.sort_dir = enumValue(params.sort_dir, 'sort_dir', SORT_DIRECTIONS, lowerCase);
820
+ return normalized;
821
+ }
822
+ function normalizeLimitDownPool(params) {
823
+ return normalizePoolQuery(params, LIMIT_DOWN_SORT_FIELDS);
824
+ }
825
+ function normalizeLimitBreakPool(params) {
826
+ return normalizePoolQuery(params, LIMIT_BREAK_SORT_FIELDS);
827
+ }
828
+ /** 飙升榜 / 热股榜:period 取 day(日/24 小时级)或 hour(小时级)。 */
829
+ function normalizeRankList(params) {
830
+ const normalized = {};
831
+ if (params.period !== undefined)
832
+ normalized.period = enumValue(params.period, 'period', RANK_PERIODS, lowerCase);
833
+ return normalized;
834
+ }
835
+ function normalizeHotStockHistory(params) {
836
+ return { date: dateString(requiredText(params.date, 'date', 32), 'date') };
837
+ }
838
+ function normalizeHotStockRankTrend(params) {
839
+ const startDate = dateString(requiredText(params.start_date, 'start_date', 32), 'start_date');
840
+ const endDate = dateString(requiredText(params.end_date, 'end_date', 32), 'end_date');
841
+ if (startDate > endDate)
842
+ fail('parameter end_date must be >= start_date');
843
+ return {
844
+ thscode: codeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
845
+ start_date: startDate,
846
+ end_date: endDate,
847
+ };
848
+ }
849
+ /** 异动标签:大小写不敏感、去重保序;空 token 与未知标签都拒绝(上游对二者都返回 1002)。 */
850
+ function anomalyTagList(value, name) {
851
+ const raw = requiredText(value, name, MAX_TEXT_LENGTH);
852
+ const tokens = raw.split(',').map((token) => token.trim().toUpperCase());
853
+ if (tokens.some((token) => token.length === 0))
854
+ fail(`parameter ${name} must not contain empty tokens`);
855
+ const unique = [];
856
+ for (const token of tokens) {
857
+ if (!ANOMALY_TAGS.includes(token))
858
+ fail(`parameter ${name} has an invalid value: ${token} (allowed: ${ANOMALY_TAGS.join(', ')})`);
859
+ if (!unique.includes(token))
860
+ unique.push(token);
861
+ }
862
+ return unique.join(',');
863
+ }
864
+ function normalizeAnomalyList(params) {
865
+ const normalized = {};
866
+ if (params.tag_codes !== undefined)
867
+ normalized.tag_codes = anomalyTagList(params.tag_codes, 'tag_codes');
868
+ return normalized;
869
+ }
870
+ function normalizeAnomalyStock(params) {
871
+ return { thscodes: codeList(params.thscodes, 'thscodes', { maximum: MAX_ANOMALY_CODES }) };
872
+ }
873
+ function normalizeDragonTiger(params) {
874
+ const normalized = {};
875
+ if (params.board_type !== undefined)
876
+ normalized.board_type = enumValue(params.board_type, 'board_type', DRAGON_TIGER_BOARDS, lowerCase);
877
+ if (params.date !== undefined)
878
+ normalized.date = dateString(params.date, 'date');
879
+ return normalized;
880
+ }
881
+ function normalizeIndexHistory(params) {
882
+ const normalized = {
883
+ thscode: indexCodeToken(requiredText(params.thscode, 'thscode', 32), 'thscode'),
884
+ interval: requiredEnum(params.interval, 'interval', KLINE_INTERVALS),
885
+ };
886
+ const start = requiredInteger(params.start, 'start', { minimum: 0 });
887
+ const end = requiredInteger(params.end, 'end', { minimum: 0 });
888
+ if (end < start)
889
+ fail('parameter end must be >= start');
890
+ if (end - start > MAX_HISTORY_WINDOW_MS)
891
+ fail('parameter window (end - start) exceeds the 10 year limit');
892
+ normalized.start = start;
893
+ normalized.end = end;
894
+ return normalized;
895
+ }
896
+ function endpointDefinitions() {
897
+ return [
898
+ {
899
+ name: 'get_meta_tickers_search',
900
+ capability: 'ticker_search',
901
+ summary: '名称或代码检索消歧为完整 thscode',
902
+ description: '按名称、代码或简称检索并消歧为完整 thscode(禁止自行拼接交易所后缀,先消歧再请求业务数据)。q 必填(去空白后 1~512 字符);exchange 支持证券交易所(SH/SZ/BJ/SSE/SZSE)与期货交易所(CFFEX/SHFE/INE/DCE/CZCE/GFEX);asset_type 逗号分隔,取值 a-share、a-share-index、forex、fund-otc、fund-etf、fund-lof、fund-reits、futures、options(**期货/期权仅可消歧,本 preset 不提供其数据端点**);limit 取值 1~50,默认 10。',
903
+ path: '/api/meta/tickers/search',
904
+ inputSchema: objectSchema({
905
+ q: { type: 'string', description: '检索关键字:中文名、纯 ticker 或完整 thscode(如 贵州茅台 / 600519 / 600519.SH)' },
906
+ exchange: { type: 'string', enum: SEARCH_EXCHANGES, description: '交易所过滤(大小写不敏感):SH / SZ / BJ / SSE / SZSE 与期货交易所 CFFEX / SHFE / INE / DCE / CZCE / GFEX' },
907
+ asset_type: { type: 'string', description: '资产类型过滤(逗号分隔,去重保序):a-share、a-share-index、forex、fund-otc、fund-etf、fund-lof、fund-reits、futures、options;本 preset 只对股票/指数/基金提供数据端点,futures/options 仅可用于消歧' },
908
+ limit: { type: 'integer', minimum: 1, maximum: 50, description: '返回条数上限,默认 10,最大 50' },
909
+ }, ['q']),
910
+ outputSchema: searchOutput,
911
+ allowed: ['q', 'exchange', 'asset_type', 'limit'],
912
+ normalize: normalizeTickerSearch,
913
+ guard: { item: { thscode: 'text', name: 'text' } },
914
+ },
915
+ {
916
+ name: 'get_a_share_prices_snapshot',
917
+ capability: 'quote',
918
+ cacheMaxAgeMs: 60_000,
919
+ summary: 'A 股行情快照(指定标的或全市场分页)',
920
+ description: '获取 A 股行情快照。两种模式:①指定标的:thscodes 传逗号分隔的完整代码(如 600519.SH,000001.SZ;自动 trim/大写/去重,裸代码被拒),此模式下 limit/offset 按上游语义忽略;②全市场遍历:省略 thscodes,用 limit/offset(limit >= 1、offset >= 0)逐页取数。快照不含股票名称。',
921
+ path: '/api/a-share/prices/snapshot',
922
+ inputSchema: objectSchema({
923
+ thscodes: { type: 'string', description: '逗号分隔的完整交易所代码(如 600519.SH,000001.SZ),需先经 ticker_search 消歧' },
924
+ limit: { type: 'integer', minimum: 1, description: '全市场模式分页大小,默认 100' },
925
+ offset: { type: 'integer', minimum: 0, description: '全市场模式分页游标,默认 0' },
926
+ }),
927
+ outputSchema: snapshotOutput,
928
+ allowed: ['thscodes', 'limit', 'offset'],
929
+ normalize: normalizeQuote,
930
+ guard: { item: { last_price: 'number' } },
931
+ paginated: true,
932
+ },
933
+ {
934
+ name: 'get_a_share_prices_historical',
935
+ capability: 'history',
936
+ summary: '单只标的日 K 线(可复权,窗口 ≤10 年)',
937
+ description: '获取单只标的历史日 K 线。thscode 必须是单只完整代码(不可逗号,自动大写);interval 当前固定支持 1d(日线);start/end 为毫秒级 Unix 时间戳(Asia/Shanghai),要求 end >= start 且单次跨度不超过 10 年;adjust 默认 forward(前复权);offset >= 0。',
938
+ path: '/api/a-share/prices/historical',
939
+ inputSchema: objectSchema({
940
+ thscode: { type: 'string', description: '单只标的完整代码(如 600519.SH),不可多只/逗号' },
941
+ interval: { type: 'string', enum: KLINE_INTERVALS, description: 'K 线周期,当前固定支持 1d(日线)' },
942
+ start: { type: 'integer', minimum: 0, description: '起始毫秒级 Unix 时间戳(Asia/Shanghai,例如 2026-08-31 00:00:00 为 1788134400000 量级)' },
943
+ end: { type: 'integer', minimum: 0, description: '截止毫秒级 Unix 时间戳;end >= start;单次跨度不超过 10 年' },
944
+ adjust: { type: 'string', enum: ADJUST_MODES, description: '复权模式:none 不复权 / forward 前复权(默认)/ backward 后复权' },
945
+ offset: { type: 'integer', minimum: 0, description: '分页偏移,默认 0' },
946
+ }, ['thscode', 'interval', 'start', 'end']),
947
+ outputSchema: klineOutput,
948
+ allowed: ['thscode', 'interval', 'start', 'end', 'adjust', 'offset'],
949
+ normalize: normalizeHistory,
950
+ guard: { item: { date_ms: 'number', close_price: 'number' } },
951
+ paginated: true,
952
+ },
953
+ {
954
+ name: 'get_a_share_calendar_trading_days',
955
+ capability: 'trading_calendar',
956
+ summary: '近一年 A 股交易日历',
957
+ description: '获取近一年(365 天)A 股交易日历;无需任何参数,传参会被拒绝。返回 item[]:date_ms 为交易日零点毫秒戳,date 为 YYYYMMDD 字符串。',
958
+ path: '/api/a-share/calendar/trading-days',
959
+ inputSchema: objectSchema({}),
960
+ outputSchema: calendarOutput,
961
+ allowed: [],
962
+ normalize: () => ({}),
963
+ guard: { item: { date_ms: 'number', date: 'text' } },
964
+ },
965
+ // ── Phase 2:代码表与除复权事件 ──────────────────────────────────────────
966
+ {
967
+ name: 'get_meta_tickers_list',
968
+ capability: 'ticker_list',
969
+ summary: '按资产类型分页拉取标的代码表',
970
+ description: '按资产类型分页批量获取标的代码表(含上市/到期/最后交易日等日期字段)。asset_type 逗号分隔,取值 a-share、a-share-index、fund-otc、fund-etf、fund-lof、fund-reits、forex、futures、options,省略返回全部类型;limit 取值 1~10000(默认 1000),offset >= 0。调用方自行递增 offset 取尽(item.length < limit 即到末页)。场外基金的 exchange 为 null。',
971
+ path: '/api/meta/tickers/list',
972
+ inputSchema: objectSchema({
973
+ asset_type: { type: 'string', description: '资产类型(逗号分隔,去重保序):a-share、a-share-index、fund-otc、fund-etf、fund-lof、fund-reits、forex、futures、options;省略返回全部' },
974
+ limit: { type: 'integer', minimum: 1, maximum: 10000, description: '单页条数,默认 1000,最大 10000' },
975
+ offset: { type: 'integer', minimum: 0, description: '分页偏移,默认 0' },
976
+ }),
977
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(tickerFields) }),
978
+ allowed: ['asset_type', 'limit', 'offset'],
979
+ normalize: normalizeTickerList,
980
+ guard: { item: tickerFields },
981
+ paginated: true,
982
+ },
983
+ {
984
+ name: 'get_a_share_corporate_actions_adjustment_factors',
985
+ capability: 'corporate_actions',
986
+ summary: '单只标的除复权事件流(分红/送转)',
987
+ description: '获取单只标的的除复权事件流(现金分红/送转股/配股原始事件,按 ex_date_ms 降序)。thscode 必须是单只完整代码(不可逗号);from/to 为 YYYY-MM-DD,可单边省略,to >= from。响应不返回 event_type 与复权因子:事件类型由 dividend_per_share 与 per_share_bonus 隐式区分,复权因子需调用方自行推导;若只要复权后价格,请用 history 并传 adjust。',
988
+ path: '/api/a-share/corporate-actions/adjustment-factors',
989
+ inputSchema: objectSchema({
990
+ thscode: { type: 'string', description: '单只标的完整代码(如 600519.SH),不接受逗号' },
991
+ from: { type: 'string', description: '事件起始日,格式 YYYY-MM-DD(可省略)' },
992
+ to: { type: 'string', description: '事件截止日,格式 YYYY-MM-DD(可省略);to >= from' },
993
+ }, ['thscode']),
994
+ outputSchema: {
995
+ type: 'object',
996
+ properties: {
997
+ thscode: { type: 'string' },
998
+ ticker: { type: 'string' },
999
+ item: { type: 'array', items: rowSchema({ ticker: 'text?', ex_date_ms: 'number', dividend_per_share: 'number?', per_share_bonus: 'number?' }) },
1000
+ },
1001
+ additionalProperties: true,
1002
+ },
1003
+ allowed: ['thscode', 'from', 'to'],
1004
+ normalize: normalizeCorporateActions,
1005
+ guard: {
1006
+ data: { thscode: 'text', ticker: 'text?' },
1007
+ item: { ticker: 'text?', ex_date_ms: 'number', dividend_per_share: 'number?', per_share_bonus: 'number?' },
1008
+ },
1009
+ },
1010
+ // ── Phase 2:财务三表与财务指标 ──────────────────────────────────────────
1011
+ {
1012
+ name: 'get_a_share_financials_income_statements',
1013
+ capability: 'income_statement',
1014
+ summary: '合并利润表多期序列',
1015
+ description: 'A 股合并利润表多期序列(按报告期降序)。单只标的:thscode 不接受逗号;period 取 annual(仅年报)或 quarterly(各季度末);两种取数模式互斥——「最近 N 期」传 limit(1~20,默认 4),「时间区间」传 start+end(毫秒戳,成对出现,跨度不超过 10 年)。金额单位为原币元,basic_eps 为元/股;report_date_ms 是披露日、period_end_ms 是报告期末,做归因/回测须用披露日防前瞻偏差;未披露字段为 null,不补零。',
1016
+ path: '/api/a-share/financials/income-statements',
1017
+ inputSchema: objectSchema({
1018
+ thscode: { type: 'string', description: '单只标的完整代码(如 600519.SH),不接受逗号' },
1019
+ period: { type: 'string', enum: REPORT_PERIODS, description: '报告期类型:annual(仅 Q4)/ quarterly(每个季度末)' },
1020
+ limit: { type: 'integer', minimum: 1, maximum: 20, description: '最近 N 期模式,默认 4;与 start/end 互斥' },
1021
+ start: { type: 'integer', minimum: 0, description: '时间区间模式起始毫秒戳,需与 end 同传;跨度不超过 10 年' },
1022
+ end: { type: 'integer', minimum: 0, description: '时间区间模式结束毫秒戳,需满足 end >= start' },
1023
+ }, ['thscode', 'period']),
1024
+ outputSchema: envelopeData({ type: 'array', items: rowSchema({ ...financialCommonFields, ...financialNumber(['operating_income', 'operating_costs', 'operating_expenses', 'sales_fee', 'manage_fee', 'research_and_development_expenses', 'operating_profit', 'interest_expenses', 'profit_total', 'income_tax_expense', 'net_profit', 'parent_holder_net_profit', 'basic_eps']) }) }),
1025
+ allowed: ['thscode', 'period', 'limit', 'start', 'end'],
1026
+ normalize: normalizeFinancial,
1027
+ guard: { item: { ...financialCommonFields, ...financialNumber(['operating_income', 'operating_costs', 'operating_expenses', 'sales_fee', 'manage_fee', 'research_and_development_expenses', 'operating_profit', 'interest_expenses', 'profit_total', 'income_tax_expense', 'net_profit', 'parent_holder_net_profit', 'basic_eps']) } },
1028
+ },
1029
+ {
1030
+ name: 'get_a_share_financials_balance_sheets',
1031
+ capability: 'balance_sheet',
1032
+ summary: '合并资产负债表多期序列',
1033
+ description: 'A 股合并资产负债表多期序列(按报告期降序)。参数与利润表完全一致(period + limit 或 start+end,互斥)。金额单位为原币元;未披露字段为 null,不补零。字段名以接口契约为准:assets_total 为资产总计、total_debt 为负债合计、holder_equity_total 为所有者权益合计。',
1034
+ path: '/api/a-share/financials/balance-sheets',
1035
+ inputSchema: objectSchema({
1036
+ thscode: { type: 'string', description: '单只标的完整代码(如 000858.SZ),不接受逗号' },
1037
+ period: { type: 'string', enum: REPORT_PERIODS, description: '报告期类型:annual(仅 Q4)/ quarterly(每个季度末)' },
1038
+ limit: { type: 'integer', minimum: 1, maximum: 20, description: '最近 N 期模式,默认 4;与 start/end 互斥' },
1039
+ start: { type: 'integer', minimum: 0, description: '时间区间模式起始毫秒戳,需与 end 同传;跨度不超过 10 年' },
1040
+ end: { type: 'integer', minimum: 0, description: '时间区间模式结束毫秒戳,需满足 end >= start' },
1041
+ }, ['thscode', 'period']),
1042
+ outputSchema: envelopeData({ type: 'array', items: rowSchema({ ...financialCommonFields, ...financialNumber(['assets_total', 'total_current_assets', 'non_current_nets_total', 'cash', 'accounts_receivable', 'total_debt', 'holder_equity_total']) }) }),
1043
+ allowed: ['thscode', 'period', 'limit', 'start', 'end'],
1044
+ normalize: normalizeFinancial,
1045
+ guard: { item: { ...financialCommonFields, ...financialNumber(['assets_total', 'total_current_assets', 'non_current_nets_total', 'cash', 'accounts_receivable', 'total_debt', 'holder_equity_total']) } },
1046
+ },
1047
+ {
1048
+ name: 'get_a_share_financials_cash_flow_statements',
1049
+ capability: 'cash_flow',
1050
+ summary: '合并现金流量表多期序列',
1051
+ description: 'A 股合并现金流量表多期序列(按报告期降序)。参数与利润表完全一致(period + limit 或 start+end,互斥)。金额单位为原币元;未披露字段为 null,不补零。pay_fixed_assets_etc_cash 为资本开支口径、pay_dividends_profits_interest_cash 为分配股利/利润/偿付利息口径。',
1052
+ path: '/api/a-share/financials/cash-flow-statements',
1053
+ inputSchema: objectSchema({
1054
+ thscode: { type: 'string', description: '单只标的完整代码(如 600519.SH),不接受逗号' },
1055
+ period: { type: 'string', enum: REPORT_PERIODS, description: '报告期类型:annual(仅 Q4)/ quarterly(每个季度末)' },
1056
+ limit: { type: 'integer', minimum: 1, maximum: 20, description: '最近 N 期模式,默认 4;与 start/end 互斥' },
1057
+ start: { type: 'integer', minimum: 0, description: '时间区间模式起始毫秒戳,需与 end 同传;跨度不超过 10 年' },
1058
+ end: { type: 'integer', minimum: 0, description: '时间区间模式结束毫秒戳,需满足 end >= start' },
1059
+ }, ['thscode', 'period']),
1060
+ outputSchema: envelopeData({ type: 'array', items: rowSchema({ ...financialCommonFields, ...financialNumber(['act_cash_flow_net', 'invest_cash_flow_net', 'financing_cash_flow_net', 'pay_fixed_assets_etc_cash', 'pay_dividends_profits_interest_cash', 'cash_equivalents_net_addition']) }) }),
1061
+ allowed: ['thscode', 'period', 'limit', 'start', 'end'],
1062
+ normalize: normalizeFinancial,
1063
+ guard: { item: { ...financialCommonFields, ...financialNumber(['act_cash_flow_net', 'invest_cash_flow_net', 'financing_cash_flow_net', 'pay_fixed_assets_etc_cash', 'pay_dividends_profits_interest_cash', 'cash_equivalents_net_addition']) } },
1064
+ },
1065
+ {
1066
+ name: 'get_a_share_financials_indicators',
1067
+ capability: 'financial_indicators',
1068
+ summary: '单期五类财务指标(成长/盈利/偿债/营运/现金流)',
1069
+ description: '获取单只标的指定报告期的五类财务指标(growth/profitability/solvency/operation/cash-flow)。thscode 不接受逗号;report 格式 yyyy-1..yyyy-4(1 一季报/2 中报/3 三季报/4 年报)。返回 data.abilities[].indicators[],每项含 index_id 与 value:value 是数据源原始数值**字符串**(保留精度与尾随小数位,不转 number),缺失为 null;百分比类指标按百分数原值表达(如 89.12000000 表示 89.12%),周转率按次、已获利息倍数按倍,展示时按 index_id 识别单位。',
1070
+ path: '/api/a-share/financials/indicators',
1071
+ inputSchema: objectSchema({
1072
+ thscode: { type: 'string', description: '单只标的完整代码(如 300033.SZ),不接受逗号' },
1073
+ report: { type: 'string', description: '报告期,格式 yyyy-1..yyyy-4(1 一季报、2 中报、3 三季报、4 年报),如 2025-1' },
1074
+ }, ['thscode', 'report']),
1075
+ outputSchema: {
1076
+ type: 'object',
1077
+ properties: {
1078
+ thscode: { type: 'string' },
1079
+ report: { type: 'string' },
1080
+ abilities: {
1081
+ type: 'array',
1082
+ items: {
1083
+ type: 'object',
1084
+ properties: {
1085
+ ability: { type: 'string' },
1086
+ indicators: {
1087
+ type: 'array',
1088
+ items: { type: 'object', properties: { index_id: { type: 'string' }, value: { oneOf: [{ type: 'string' }, { type: 'null' }] } }, additionalProperties: true },
1089
+ },
1090
+ },
1091
+ additionalProperties: true,
1092
+ },
1093
+ },
1094
+ },
1095
+ additionalProperties: true,
1096
+ },
1097
+ allowed: ['thscode', 'report'],
1098
+ normalize: normalizeFinancialIndicators,
1099
+ guard: { data: { thscode: 'text', report: 'text' }, custom: validateAbilities },
1100
+ },
1101
+ // ── Phase 2:估值与集合竞价 ─────────────────────────────────────────────
1102
+ {
1103
+ name: 'get_a_share_valuations_snapshot',
1104
+ capability: 'valuation',
1105
+ cacheMaxAgeMs: 60_000,
1106
+ summary: '批量估值快照(PE/PB/PS/PCF)',
1107
+ description: '批量获取 A 股最新估值快照(固定 5 个指标:市盈率 TTM/MRQ、市净率 MRQ、市销率 TTM、市现率 TTM)。thscodes 必填,逗号分隔的完整代码(6 位数字 + .SH/.SZ/.BJ),单次最多 100 个原始 token(去重前校验,服务端按请求顺序去重返回)。不提供历史估值、分页或指标选择;上游缺失的指标为 null,不补零;未返回的股票不会生成占位项(无匹配时 item 为空数组)。',
1108
+ path: '/api/a-share/valuations/snapshot',
1109
+ inputSchema: objectSchema({
1110
+ thscodes: { type: 'string', description: '逗号分隔的完整代码,如 600519.SH,000001.SZ;单次最多 100 个' },
1111
+ }, ['thscodes']),
1112
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(valuationFields) }),
1113
+ allowed: ['thscodes'],
1114
+ normalize: normalizeValuation,
1115
+ guard: { item: valuationFields },
1116
+ },
1117
+ {
1118
+ name: 'get_a_share_auction_snapshot',
1119
+ capability: 'auction',
1120
+ cacheMaxAgeMs: 60_000,
1121
+ summary: '集合竞价快照(实时盘口或终态)',
1122
+ description: '获取 A 股集合竞价快照。thscodes 必填,逗号分隔的完整代码,单次最多 100 个原始 token;stage 取 live(实时盘口)或 final(9:25 最终撮合形态,默认 final)。返回 data.auction_phase / data.data_status 与竞价明细:auction_pct 为涨跌幅百分数原值,auction_volume/auction_unmatched 单位为股,auction_amount 单位为元,auction_turnover_pct 为竞价换手率百分数原值,auction_volume_ratio 为竞价量比。',
1123
+ path: '/api/a-share/auction/snapshot',
1124
+ inputSchema: objectSchema({
1125
+ thscodes: { type: 'string', description: '逗号分隔的完整代码,如 600519.SH,000001.SZ;单次最多 100 个' },
1126
+ stage: { type: 'string', enum: AUCTION_STAGES, description: '竞价阶段:live 实时盘口 / final 终态(默认 final)' },
1127
+ }, ['thscodes']),
1128
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(auctionFields) }),
1129
+ allowed: ['thscodes', 'stage'],
1130
+ normalize: normalizeAuction,
1131
+ guard: { data: { auction_phase: 'text?', data_status: 'text?' }, item: auctionFields },
1132
+ },
1133
+ // ── Phase 2:盘面特色数据 ───────────────────────────────────────────────
1134
+ {
1135
+ name: 'get_a_share_special_data_limit_up_pool',
1136
+ capability: 'limit_up_pool',
1137
+ summary: '按交易日分页的涨停/连板股票池',
1138
+ description: '按交易日获取 A 股涨停/连板股票池(后端固定取全部连板与 main/chinext/ssestar/north 四类板块)。date_ms 为交易日上海时区零点毫秒戳(省略回退服务端当日);page >= 1;size 取值 1~200(默认 50);sort_field 取 last_price / continue_day_cnt / seal_money / limit_up_time,sort_dir 取 asc / desc。响应的 data.pagination 给出上游 total/pages/size/page——它描述上游结果集,不等于本 Dataset 的行数;单次请求只取一页,不做自动翻页。limit_up_reason 上游空串会标准化为 null;seal_money/max_seal_money 单位为元。',
1139
+ path: '/api/a-share/special-data/limit-up-pool',
1140
+ inputSchema: objectSchema({
1141
+ date_ms: { type: 'integer', minimum: 0, description: '交易日上海时区零点毫秒戳;省略回退服务端当前自然日' },
1142
+ page: { type: 'integer', minimum: 1, description: '页码,从 1 开始,默认 1' },
1143
+ size: { type: 'integer', minimum: 1, maximum: 200, description: '单页条数,取值 1~200,默认 50' },
1144
+ sort_field: { type: 'string', enum: LIMIT_UP_SORT_FIELDS, description: '排序字段,默认 last_price' },
1145
+ sort_dir: { type: 'string', enum: SORT_DIRECTIONS, description: '排序方向,默认 desc' },
1146
+ }),
1147
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(limitUpPoolFields) }),
1148
+ allowed: ['date_ms', 'page', 'size', 'sort_field', 'sort_dir'],
1149
+ normalize: normalizeLimitUpPool,
1150
+ guard: { data: { pagination: 'object' }, item: limitUpPoolFields },
1151
+ paginated: true,
1152
+ },
1153
+ {
1154
+ name: 'get_a_share_special_data_limit_up_ladder',
1155
+ capability: 'limit_up_ladder',
1156
+ summary: '近 30 日连板天梯矩阵',
1157
+ description: '获取 A 股近 30 个交易日的连板天梯矩阵(无入参,上游固定 30 日、每个板位最多 4 只)。data.window 给出窗口长度、交易日列表与各板位容量;data.item[] 按交易日组织,每项含 date 与 boards:two_board、three_board、four_board、five_board、six_board、seven_over 六个数组(上游缺失的板位补 []),元素含 thscode/ticker/name/board_num/seal_nextday/sign_level,其中 seal_nextday 表示次一交易日是否继续封板(最近交易日无次日参考,固定为 null)。',
1158
+ path: '/api/a-share/special-data/limit-up-ladder',
1159
+ inputSchema: objectSchema({}),
1160
+ outputSchema: {
1161
+ type: 'object',
1162
+ properties: {
1163
+ timestamp: { type: 'integer' },
1164
+ window: { type: 'object', additionalProperties: true },
1165
+ item: {
1166
+ type: 'array',
1167
+ items: {
1168
+ type: 'object',
1169
+ properties: { date: { type: 'string' }, boards: { type: 'object', additionalProperties: true } },
1170
+ additionalProperties: true,
1171
+ },
1172
+ },
1173
+ },
1174
+ additionalProperties: true,
1175
+ },
1176
+ allowed: [],
1177
+ normalize: () => ({}),
1178
+ guard: { data: { window: 'object' }, item: ladderFields },
1179
+ },
1180
+ // ── Phase 2:指数 ───────────────────────────────────────────────────────
1181
+ {
1182
+ name: 'get_a_share_index_catalog_ths_index_list',
1183
+ capability: 'index_catalog',
1184
+ summary: '同花顺指数目录(概念/区域/特色/行业)',
1185
+ description: '按 tag 列出同花顺指数(概念/区域/特色/行业)清单,单 tag 一次性全量返回、无分页。tag 取 cn_concept(A 股概念,默认)/ region(区域)/ tszs(特色)/ industry(行业),大小写不敏感。返回 item[]:thscode 与 name;指数维度不返回纯代码 ticker。',
1186
+ path: '/api/a-share-index/catalog/ths-index-list',
1187
+ inputSchema: objectSchema({
1188
+ tag: { type: 'string', enum: INDEX_TAGS, description: '标签白名单:cn_concept / region / tszs / industry,默认 cn_concept' },
1189
+ }),
1190
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(indexCatalogFields) }),
1191
+ allowed: ['tag'],
1192
+ normalize: normalizeIndexCatalog,
1193
+ guard: { item: indexCatalogFields },
1194
+ },
1195
+ {
1196
+ name: 'get_a_share_index_constituents_ths_stock_list',
1197
+ capability: 'index_constituents',
1198
+ summary: '单个指数的成分股清单',
1199
+ description: '按单个指数代码获取当前成分股清单。thscode 为指数代码(交易所指数如 000300.SH / 399300.SZ,或同花顺板块如 886042.TI),不接受逗号,入参会 trim 并转大写。返回 item[]:成分股 thscode/ticker/name。',
1200
+ path: '/api/a-share-index/constituents/ths-stock-list',
1201
+ inputSchema: objectSchema({
1202
+ thscode: { type: 'string', description: '指数完整代码,如 000300.SH、399300.SZ 或 886042.TI;不接受逗号' },
1203
+ }, ['thscode']),
1204
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(indexConstituentFields) }),
1205
+ allowed: ['thscode'],
1206
+ normalize: normalizeIndexConstituents,
1207
+ guard: { item: indexConstituentFields },
1208
+ },
1209
+ {
1210
+ name: 'get_a_share_index_prices_snapshot',
1211
+ capability: 'index_quote',
1212
+ cacheMaxAgeMs: 60_000,
1213
+ summary: '指数行情快照(批量)',
1214
+ description: '按 thscodes 批量获取指数最新行情快照(覆盖交易所指数与同花顺板块/行业指数)。thscodes 必填,逗号分隔,不接受空入参枚举全指数;limit/offset 对本接口无效,故不暴露。返回 item[] 字段结构与 A 股行情快照一致(last_price/涨跌/开高低/前收/成交量额),不含指数名称。',
1215
+ path: '/api/a-share-index/prices/snapshot',
1216
+ inputSchema: objectSchema({
1217
+ thscodes: { type: 'string', description: '逗号分隔的指数代码,如 000001.SH,399001.SZ,886042.TI' },
1218
+ }, ['thscodes']),
1219
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(indexQuoteFields) }),
1220
+ allowed: ['thscodes'],
1221
+ normalize: normalizeIndexQuote,
1222
+ guard: { item: indexQuoteFields },
1223
+ },
1224
+ {
1225
+ name: 'get_a_share_index_prices_historical',
1226
+ capability: 'index_history',
1227
+ summary: '单只指数日 K 线(窗口 ≤10 年)',
1228
+ description: '获取单只指数的历史 K 线序列,只支持时间区间模式。thscode 为单只指数代码(如 000001.SH / 886042.TI),不接受逗号;interval 当前仅支持 1d;start/end 为毫秒级 Unix 时间戳,end >= start 且跨度不超过 10 年。指数无复权语义:本接口没有 adjust,也没有 offset;响应中的 data.adjust 固定为 null。',
1229
+ path: '/api/a-share-index/prices/historical',
1230
+ inputSchema: objectSchema({
1231
+ thscode: { type: 'string', description: '单只指数完整代码(如 000001.SH、886042.TI),不接受逗号' },
1232
+ interval: { type: 'string', enum: KLINE_INTERVALS, description: 'K 线周期,当前仅支持 1d' },
1233
+ start: { type: 'integer', minimum: 0, description: '起始毫秒级 Unix 时间戳' },
1234
+ end: { type: 'integer', minimum: 0, description: '结束毫秒级 Unix 时间戳;end >= start;跨度不超过 10 年' },
1235
+ }, ['thscode', 'interval', 'start', 'end']),
1236
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(indexHistoryFields) }),
1237
+ allowed: ['thscode', 'interval', 'start', 'end'],
1238
+ normalize: normalizeIndexHistory,
1239
+ guard: { item: indexHistoryFields },
1240
+ },
1241
+ // ── Phase 3:盘面特色数据(doc 详情段 + 真实探针确认可用)────────────────
1242
+ {
1243
+ name: 'get_a_share_special_data_limit_down_pool',
1244
+ capability: 'limit_down_pool',
1245
+ summary: '按交易日分页的跌停股票池',
1246
+ description: '按交易日获取 A 股跌停股票池(首次/最后跌停时间为上海时区 HH:mm)。date_ms 为交易日上海时区零点毫秒戳(省略回退服务端当日);page >= 1;size 取值 1~200(默认 50);sort_field 取 last_limit_time / first_limit_time / last_price / price_change_ratio_pct / turnover_ratio_pct,sort_dir 取 asc / desc。data.pagination 描述上游结果集,不等于本 Dataset 行数;单次只取一页,不自动翻页。turnover_ratio_pct 为换手率百分数原值。',
1247
+ path: '/api/a-share/special-data/limit-down-pool',
1248
+ inputSchema: objectSchema({
1249
+ date_ms: { type: 'integer', minimum: 0, description: '交易日上海时区零点毫秒戳;省略回退服务端当前自然日' },
1250
+ page: { type: 'integer', minimum: 1, description: '页码,从 1 开始,默认 1' },
1251
+ size: { type: 'integer', minimum: 1, maximum: 200, description: '单页条数,取值 1~200,默认 50' },
1252
+ sort_field: { type: 'string', enum: LIMIT_DOWN_SORT_FIELDS, description: '排序字段,默认 last_limit_time' },
1253
+ sort_dir: { type: 'string', enum: SORT_DIRECTIONS, description: '排序方向,默认 desc' },
1254
+ }),
1255
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(limitDownFields) }),
1256
+ allowed: ['date_ms', 'page', 'size', 'sort_field', 'sort_dir'],
1257
+ normalize: normalizeLimitDownPool,
1258
+ guard: { data: { pagination: 'object' }, item: limitDownFields },
1259
+ paginated: true,
1260
+ },
1261
+ {
1262
+ name: 'get_a_share_special_data_limit_break_pool',
1263
+ capability: 'limit_break_pool',
1264
+ summary: '按交易日分页的炸板(涨停破板)股票池',
1265
+ description: '按交易日获取 A 股涨停炸板股票池(接口直接消费炸板集合,不做服务端归因)。date_ms 为交易日上海时区零点毫秒戳(省略回退服务端当日);page >= 1;size 取值 1~200(默认 50);sort_field 取 price_change_ratio_pct / open_times / last_price / turnover_ratio_pct / turnover,sort_dir 取 asc / desc。open_times 为盘中开板次数;turnover 为成交额(元);data.pagination 描述上游结果集,不等于本 Dataset 行数。',
1266
+ path: '/api/a-share/special-data/limit-break-pool',
1267
+ inputSchema: objectSchema({
1268
+ date_ms: { type: 'integer', minimum: 0, description: '交易日上海时区零点毫秒戳;省略回退服务端当前自然日' },
1269
+ page: { type: 'integer', minimum: 1, description: '页码,从 1 开始,默认 1' },
1270
+ size: { type: 'integer', minimum: 1, maximum: 200, description: '单页条数,取值 1~200,默认 50' },
1271
+ sort_field: { type: 'string', enum: LIMIT_BREAK_SORT_FIELDS, description: '排序字段,默认 price_change_ratio_pct' },
1272
+ sort_dir: { type: 'string', enum: SORT_DIRECTIONS, description: '排序方向,默认 desc' },
1273
+ }),
1274
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(limitBreakFields) }),
1275
+ allowed: ['date_ms', 'page', 'size', 'sort_field', 'sort_dir'],
1276
+ normalize: normalizeLimitBreakPool,
1277
+ guard: { data: { pagination: 'object' }, item: limitBreakFields },
1278
+ paginated: true,
1279
+ },
1280
+ {
1281
+ name: 'get_a_share_special_data_skyrocket_list',
1282
+ capability: 'skyrocket_list',
1283
+ summary: 'A 股热度飙升榜 Top30(日榜/小时榜)',
1284
+ description: '查询 A 股热度排名飙升榜 Top30。period 取 day(日榜)或 hour(小时榜),省略默认 day。返回 item[] 最多 30 条:rank 为当前排名,heat 是上游原始**字符串**热度值(保留精度,不转数字),rank_change 为排名变化(正数上升、负数下降,上游缺失为 null),rank_trend 取 up / down / flat / unknown。',
1285
+ path: '/api/a-share/special-data/skyrocket-list',
1286
+ inputSchema: objectSchema({
1287
+ period: { type: 'string', enum: RANK_PERIODS, description: '榜单周期:day 日榜(默认)/ hour 小时榜' },
1288
+ }),
1289
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(rankListFields) }),
1290
+ allowed: ['period'],
1291
+ normalize: normalizeRankList,
1292
+ guard: { item: rankListFields },
1293
+ },
1294
+ {
1295
+ name: 'get_a_share_special_data_hot_stock_list',
1296
+ capability: 'hot_stock_list',
1297
+ summary: 'A 股热股榜 Top30(24 小时/小时级)',
1298
+ description: '查询 A 股热股榜单 Top30。period 取 day(24 小时级别,默认)或 hour(小时级别)。响应结构与飙升榜一致:rank 排名、heat 上游原始字符串热度值、rank_change 排名变化(可为 null)、rank_trend 取 up / down / flat / unknown。',
1299
+ path: '/api/a-share/special-data/hot-stock-list',
1300
+ inputSchema: objectSchema({
1301
+ period: { type: 'string', enum: RANK_PERIODS, description: '榜单周期:day 24 小时级别(默认)/ hour 小时级别' },
1302
+ }),
1303
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(rankListFields) }),
1304
+ allowed: ['period'],
1305
+ normalize: normalizeRankList,
1306
+ guard: { item: rankListFields },
1307
+ },
1308
+ {
1309
+ name: 'get_a_share_special_data_hot_stock_list_history',
1310
+ capability: 'hot_stock_history',
1311
+ summary: '按自然日的历史热股榜排行(最多 30 条)',
1312
+ description: '按自然日返回历史热股榜排行,最多 30 条。date 必填,格式 yyyy-MM-dd,只支持一年内数据(上游对外只接受日期字符串,避免调用方传时间戳导致未对齐自然日)。data 含 date(查询自然日)与 date_ms(该日上海时区零点毫秒戳);item[] 只有 thscode / ticker / name / rank。',
1313
+ path: '/api/a-share/special-data/hot-stock-list-history',
1314
+ inputSchema: objectSchema({
1315
+ date: { type: 'string', description: '目标自然日,格式 yyyy-MM-dd,只支持一年内数据' },
1316
+ }, ['date']),
1317
+ outputSchema: {
1318
+ type: 'object',
1319
+ properties: {
1320
+ date: { type: 'string' },
1321
+ date_ms: { type: 'integer' },
1322
+ item: { type: 'array', items: rowSchema(hotStockHistoryFields) },
1323
+ },
1324
+ additionalProperties: true,
1325
+ },
1326
+ allowed: ['date'],
1327
+ normalize: normalizeHotStockHistory,
1328
+ guard: { data: { date: 'text', date_ms: 'number' }, item: hotStockHistoryFields },
1329
+ },
1330
+ {
1331
+ name: 'get_a_share_special_data_hot_stock_rank_trend',
1332
+ capability: 'hot_stock_rank_trend',
1333
+ summary: '单只 A 股的热榜排名走势(日线点位)',
1334
+ description: '查询单只 A 股在自然日窗口内的热榜排名走势,返回与上游日线对齐的点位、不做 Top30 截断。thscode 为单只完整代码;start_date / end_date 必填,格式 yyyy-MM-dd,要求 end_date >= start_date;日期与窗口都限制在一年内。item[] 含 date(自然日)、date_ms 与当日 rank。',
1335
+ path: '/api/a-share/special-data/hot-stock-rank-trend',
1336
+ inputSchema: objectSchema({
1337
+ thscode: { type: 'string', description: '单只 A 股完整代码(如 300034.SZ),不接受逗号' },
1338
+ start_date: { type: 'string', description: '起始自然日,格式 yyyy-MM-dd' },
1339
+ end_date: { type: 'string', description: '结束自然日,格式 yyyy-MM-dd;需 >= start_date;窗口不超过一年' },
1340
+ }, ['thscode', 'start_date', 'end_date']),
1341
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(rankTrendFields) }),
1342
+ allowed: ['thscode', 'start_date', 'end_date'],
1343
+ normalize: normalizeHotStockRankTrend,
1344
+ guard: { item: rankTrendFields },
1345
+ },
1346
+ {
1347
+ name: 'get_a_share_special_data_anomaly_analysis_list',
1348
+ capability: 'anomaly_list',
1349
+ summary: '当日个股异动原因列表(可按标签过滤)',
1350
+ description: '查询当日个股异动原因,可选按异动标签过滤;不传 tag_codes 时返回全部当日记录。tag_codes 逗号分隔、多个值为 OR、大小写不敏感、重复值去重,合法值:LIMIT_UP 涨停 / LIMIT_DOWN 跌停 / SHARP_RISE 大涨 / SHARP_FALL 大跌 / RAPID_RALLY 快速拉升 / RAPID_DECLINE 快速下挫。item[] 含 stock_name、analysis_content、keyword_list(关键词数组,可为空数组)、thscode、tag_name。当日快照未就绪返回 3002;有快照但无匹配时 item 为空数组。(该接口上游只提供 REST,不同步为 MCP 工具。)',
1351
+ path: '/api/a-share/special-data/anomaly-analysis-list',
1352
+ inputSchema: objectSchema({
1353
+ tag_codes: { type: 'string', description: '异动标签,**可传逗号分隔的多个值(OR 关系)**,大小写不敏感、重复去重。合法值:LIMIT_UP / LIMIT_DOWN / SHARP_RISE / SHARP_FALL / RAPID_RALLY / RAPID_DECLINE;不传表示全部当日记录。注意本参数不是单值枚举,可组合如 LIMIT_UP,SHARP_FALL' },
1354
+ }),
1355
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(anomalyFields) }),
1356
+ allowed: ['tag_codes'],
1357
+ normalize: normalizeAnomalyList,
1358
+ guard: { item: anomalyFields },
1359
+ },
1360
+ {
1361
+ name: 'get_a_share_special_data_anomaly_analysis_stock',
1362
+ capability: 'anomaly_stock',
1363
+ summary: '按股票批量查询当日异动原因',
1364
+ description: '按同花顺代码批量查询当日个股异动原因,按请求代码首次出现顺序返回匹配记录;格式合法但当日无异动的代码会被忽略。thscodes 必填,逗号分隔的完整代码(SH/SZ/BJ),去重前最多 50 个 token。字段与异动原因列表一致(stock_name / analysis_content / keyword_list / thscode / tag_name);当日快照未就绪返回 3002。',
1365
+ path: '/api/a-share/special-data/anomaly-analysis-stock',
1366
+ inputSchema: objectSchema({
1367
+ thscodes: { type: 'string', description: '逗号分隔的完整代码(如 600519.SH,000001.SZ),去重前最多 50 个' },
1368
+ }, ['thscodes']),
1369
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(anomalyFields) }),
1370
+ allowed: ['thscodes'],
1371
+ normalize: normalizeAnomalyStock,
1372
+ guard: { item: anomalyFields },
1373
+ },
1374
+ {
1375
+ name: 'get_a_share_special_data_dragon_tiger_list',
1376
+ capability: 'dragon_tiger',
1377
+ summary: 'A 股龙虎榜(全部/机构榜/游资榜,不分页)',
1378
+ description: '按交易日返回 A 股龙虎榜整体榜单,一个接口覆盖全部 / 机构榜 / 游资榜,固定全量返回、不分页。board_type 取 all / org / hot_money(默认 all);date 格式 yyyy-MM-dd,只支持一年内且必须是交易日(显式传非交易日返回 1002,不自动回退),省略时取最近可用交易日。data 含 board_type、trade_date、count(上游记录数,同一股票可能同时上当日榜与 3 日榜)、stock_count(去重数量)、stock_items[](board_type=all/org 时填充)与 hot_money_items[](board_type=hot_money 时填充);两者互斥为空数组。注意 change 与 net_rate 是**小数形式**(0.09994 表示 9.994%),与行情接口的百分数原值口径不同;概念、机构与游资净买入字段见 output_schema。',
1379
+ path: '/api/a-share/special-data/dragon-tiger-list',
1380
+ inputSchema: objectSchema({
1381
+ board_type: { type: 'string', enum: DRAGON_TIGER_BOARDS, description: '榜单类型:all 全部(默认)/ org 机构榜 / hot_money 游资榜' },
1382
+ date: { type: 'string', description: '目标交易日,格式 yyyy-MM-dd,只支持一年内;必须是交易日' },
1383
+ }),
1384
+ outputSchema: {
1385
+ type: 'object',
1386
+ properties: {
1387
+ timestamp: { type: 'integer' },
1388
+ board_type: { type: 'string' },
1389
+ trade_date: { type: 'string' },
1390
+ count: { type: 'integer' },
1391
+ stock_count: { type: 'integer' },
1392
+ stock_items: { type: 'array', items: rowSchema(dragonTigerStockFields) },
1393
+ hot_money_items: {
1394
+ type: 'array',
1395
+ items: {
1396
+ type: 'object',
1397
+ properties: {
1398
+ name: { type: 'string' },
1399
+ buying: { type: 'number' },
1400
+ rows: { type: 'array', items: rowSchema(dragonTigerStockFields) },
1401
+ },
1402
+ additionalProperties: true,
1403
+ },
1404
+ },
1405
+ },
1406
+ additionalProperties: true,
1407
+ },
1408
+ allowed: ['board_type', 'date'],
1409
+ normalize: normalizeDragonTiger,
1410
+ guard: {
1411
+ data: { board_type: 'text?', trade_date: 'text', stock_items: 'array', hot_money_items: 'array' },
1412
+ itemKey: 'stock_items',
1413
+ rowKeys: ["stock_items", "hot_money_items[].rows"],
1414
+ item: dragonTigerStockFields,
1415
+ },
1416
+ },
1417
+ // ── Phase 3:短线风向标竞价基准 ─────────────────────────────────────────
1418
+ {
1419
+ name: 'get_a_share_auction_short_term_benchmark',
1420
+ capability: 'auction_benchmark',
1421
+ summary: '短线风向标竞价基准(带标签的精选池)',
1422
+ description: '获取追踪集合竞价异动的短线风向标精选池。date 可选,格式 yyyy-MM-dd;省略时用上海时区当日,显式指定非交易日不自动回退。data 含 date(最终查询日期)与 date_ms(该日零点毫秒戳);item[] 含 thscode / ticker / name / auction_pct(竞价涨跌幅百分数原值)与 tags(标签数组,如 高开、放量)。',
1423
+ path: '/api/a-share/auction/short-term-benchmark',
1424
+ inputSchema: objectSchema({
1425
+ date: { type: 'string', description: '查询日期,格式 yyyy-MM-dd;省略用上海时区当日' },
1426
+ }),
1427
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(auctionBenchmarkFields) }),
1428
+ allowed: ['date'],
1429
+ normalize: (params) => {
1430
+ const normalized = {};
1431
+ if (params.date !== undefined)
1432
+ normalized.date = dateString(params.date, 'date');
1433
+ return normalized;
1434
+ },
1435
+ guard: { data: { date: 'text', date_ms: 'number' }, item: auctionBenchmarkFields },
1436
+ },
1437
+ // ── Phase 3B:公募基金(14 个核心端点;契约 = 文档详情段 + 真实探针)────────
1438
+ // 定位规则:这些端点全部只吃**单数 thscode**(基金代码带 .OF/.SH/.SZ 后缀),
1439
+ // 经理与公司分别用 manager_id / company_id;ths_api.md 里的 fund_type(otc/exchange/reits)
1440
+ // 实测并不存在(上游会忽略未知参数,我们不忽略)。金额/份额单位多处文档未给出,
1441
+ // 一律按原值转述、不擅自换算。
1442
+ {
1443
+ name: 'get_fund_profile_detail',
1444
+ capability: 'fund_profile',
1445
+ summary: '基金基本资料(规模/净值/经理/费率)',
1446
+ description: '获取单只基金的基本资料。thscode 为完整基金代码(025480.OF / 510300.SH / 161725.SZ),不接受逗号。item[] 含 fund_name、estab_date(成立日期,毫秒时间戳)、company_id(可用于 fund_company)、mgmt_name、manager_name、fund_scale、unit_nav,以及三个嵌套数组:manager_info(实测元素含 manager_id / manager_name / tenure_return_pct / tenure_days / start_date_ms)、trade_rule(交易规则)、rate_info(费率,实测元素含 rate_type / charge_mode / standard_rate)。fund_name / estab_date / company_id / mgmt_name / manager_name / fund_scale / unit_nav 均可为 null;fund_scale 与 unit_nav 的单位文档未给出,按原值转述。',
1447
+ path: '/api/fund/profile/detail',
1448
+ inputSchema: objectSchema({
1449
+ thscode: { type: 'string', description: '完整基金代码,必须保留市场后缀,如 025480.OF / 510300.SH / 161725.SZ' },
1450
+ }, ['thscode']),
1451
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundProfileFields) }),
1452
+ allowed: ['thscode'],
1453
+ normalize: onlyFundCode,
1454
+ guard: { item: fundProfileFields },
1455
+ },
1456
+ {
1457
+ name: 'get_fund_market_snapshot',
1458
+ capability: 'fund_quote',
1459
+ cacheMaxAgeMs: 60_000,
1460
+ summary: '场内基金(ETF)行情快照',
1461
+ description: '获取单只**场内 ETF** 的最新行情快照。thscode 必填、单只、带后缀,不接受逗号多值。**当前仅支持 ETF:LOF、场外基金、REITs 或尚未开放的基金叶子类型返回 code=3004。** item[] 含 last_price / open_price / high_price / low_price / prev_price / price_change / price_change_ratio_pct(百分数原值)/ price_amplitude_ratio_pct(振幅,百分数原值)/ volume / turnover / turnover_ratio_pct(换手率,百分数原值);价格按原始货币计价。data.timestamp 在无有效数据时为 null。',
1462
+ path: '/api/fund/market/snapshot',
1463
+ inputSchema: objectSchema({
1464
+ thscode: { type: 'string', description: '单只场内 ETF 的完整代码(如 510300.SH),不接受逗号多值' },
1465
+ }, ['thscode']),
1466
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundQuoteFields) }),
1467
+ allowed: ['thscode'],
1468
+ normalize: onlyFundCode,
1469
+ guard: { item: fundQuoteFields },
1470
+ },
1471
+ {
1472
+ name: 'get_fund_market_historical',
1473
+ capability: 'fund_history',
1474
+ summary: '场内基金(ETF)历史日 K 线',
1475
+ description: '获取单只**场内 ETF** 的历史日 K 线。thscode 必填、单只、带后缀、不接受逗号;interval 当前仅支持 1d;start / end 为毫秒级 Unix 时间戳,要求 end >= start 且**窗口最长 5 个自然年**(注意:比 A 股 history 的 10 年更短)。LOF、场外基金与 REITs 返回 code=3004。item[] 含 date_ms / 开高低收 / volume / turnover;价格为前复权口径,响应中的 data.adjust 固定为 null(该字段不是请求参数)。volume / turnover 单位文档未给出。',
1476
+ path: '/api/fund/market/historical',
1477
+ inputSchema: objectSchema({
1478
+ thscode: { type: 'string', description: '单只场内 ETF 的完整代码(如 510300.SH),不接受逗号多值' },
1479
+ interval: { type: 'string', enum: KLINE_INTERVALS, description: 'K 线周期,当前仅支持 1d' },
1480
+ start: { type: 'integer', minimum: 0, description: '起始毫秒级 Unix 时间戳' },
1481
+ end: { type: 'integer', minimum: 0, description: '结束毫秒级 Unix 时间戳;end >= start;单次窗口不超过 5 个自然年' },
1482
+ }, ['thscode', 'start', 'end']),
1483
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundHistoryFields) }),
1484
+ allowed: ['thscode', 'interval', 'start', 'end'],
1485
+ normalize: normalizeFundMarketHistory,
1486
+ guard: { item: fundHistoryFields },
1487
+ },
1488
+ {
1489
+ name: 'get_fund_performance_nav',
1490
+ capability: 'fund_nav',
1491
+ summary: '基金历史净值(单位/复权,按区间)',
1492
+ description: '获取单只基金的历史净值序列。thscode 必填带后缀;range 可选,取 week / month / tmonth / hyear / year / twoyear / tyear / fyear,省略时最多返回最新一个净值日期(给了 range 则按 nav_date 升序返回区间序列);nav_type 可选,取 unit(单位净值)/ adj(复权净值)/ unit,adj(默认,同时返回)。item[] 含 nav_date(净值日期,**毫秒时间戳**)与 unit_nav / adj_nav:**未通过 nav_type 请求的字段不会输出**,因此按需取字段。复权净值不等同于累计净值。',
1493
+ path: '/api/fund/performance/nav',
1494
+ inputSchema: objectSchema({
1495
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1496
+ range: { type: 'string', enum: NAV_RANGES, description: '区间:week/month/tmonth/hyear/year/twoyear/tyear/fyear;省略只返回最新一个净值日期' },
1497
+ nav_type: { type: 'string', enum: NAV_TYPES, description: '净值类型:unit 单位净值 / adj 复权净值 / unit,adj 同时返回(默认)' },
1498
+ }, ['thscode']),
1499
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundNavFields) }),
1500
+ allowed: ['thscode', 'range', 'nav_type'],
1501
+ normalize: normalizeFundNav,
1502
+ guard: { item: fundNavFields },
1503
+ },
1504
+ {
1505
+ name: 'get_fund_performance_returns',
1506
+ capability: 'fund_returns',
1507
+ summary: '基金阶段收益与同类排名',
1508
+ description: '获取单只基金的阶段收益率、同类平均与排名。thscode 必填带后缀。item[] 为一行多列:return_* 覆盖 week / month / tmonth / hyear / year / twoyear / tyear / fyear / nowyear / now(近一周到成立以来),同类平均为 peer_average_*,排名与参与总数为 rank_* / rank_total_*。**收益率一律为百分数原值**(8.88 表示 8.88%)。该行没有代码类主标识字段,结构与字段集合以上游返回为准。',
1509
+ path: '/api/fund/performance/returns',
1510
+ inputSchema: objectSchema({
1511
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1512
+ }, ['thscode']),
1513
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundReturnsFields) }),
1514
+ allowed: ['thscode'],
1515
+ normalize: onlyFundCode,
1516
+ guard: { custom: validateFundReturns },
1517
+ },
1518
+ {
1519
+ name: 'get_fund_performance_drawdowns',
1520
+ capability: 'fund_drawdowns',
1521
+ summary: '基金各区间最大回撤',
1522
+ description: '获取单只基金各区间最大回撤。thscode 必填带后缀。item[] 含 thscode / ticker 与十个区间字段:week / month / tmonth / hyear / year / twoyear / tyear / fyear / nowyear / now(近一周到成立以来)。**回撤为百分数原值且通常为负数**(例如 -12.5 表示回撤 12.5%),不是小数形式。',
1523
+ path: '/api/fund/performance/drawdowns',
1524
+ inputSchema: objectSchema({
1525
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1526
+ }, ['thscode']),
1527
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundDrawdownFields) }),
1528
+ allowed: ['thscode'],
1529
+ normalize: onlyFundCode,
1530
+ guard: { item: fundDrawdownFields },
1531
+ },
1532
+ {
1533
+ name: 'get_fund_portfolio_holdings',
1534
+ capability: 'fund_holdings',
1535
+ summary: '基金重仓持仓(定期披露,非实时)',
1536
+ description: '获取单只基金定期披露的持仓明细(**来自定期报告、不是实时持仓**)。thscode 必填带后缀。item[] 含持仓标的 thscode / ticker / stock_name(资产名称,字段名为兼容旧契约保留)/ hold_ratio(占基金净值比例,百分数原值)/ asset_type(stock / bond / fund)/ position_capital(持仓市值)/ position_count(持仓数量)/ security_market_value_rate_pct / period_increase_rate_pct / investment_rank / start_date_ms / end_date_ms / publish_date_ms / modify_time_ms。data 顶层**还可能包含**汇总标量 total_stock_ratio_pct、total_bond_ratio_pct、total_fund_ratio_pct、turnover_rate_pct、stock_ratio_pct、main_industry、concentration_ratio——它们条件出现,不要假定一定存在。市值/数量单位文档未给出。',
1537
+ path: '/api/fund/portfolio/holdings',
1538
+ inputSchema: objectSchema({
1539
+ thscode: { type: 'string', description: '完整基金代码(如 025480.OF 或 510300.SH)' },
1540
+ }, ['thscode']),
1541
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundHoldingFields) }),
1542
+ allowed: ['thscode'],
1543
+ normalize: onlyFundCode,
1544
+ guard: {
1545
+ data: {
1546
+ total_stock_ratio_pct: 'number?', total_bond_ratio_pct: 'number?', total_fund_ratio_pct: 'number?',
1547
+ turnover_rate_pct: 'number?', stock_ratio_pct: 'number?', main_industry: 'text?', concentration_ratio: 'number?',
1548
+ },
1549
+ item: fundHoldingFields,
1550
+ },
1551
+ },
1552
+ {
1553
+ name: 'get_fund_portfolio_asset_allocation',
1554
+ capability: 'fund_asset_allocation',
1555
+ summary: '基金资产配置比例(按报告期)',
1556
+ description: '获取单只基金按报告期的资产配置比例。thscode 必填带后缀。item[] 含 report_date_ms(报告期,毫秒时间戳)与 stock_ratio_pct / bond_ratio_pct / deposit_ratio_pct / other_ratio_pct(股票/债券/存款/其他占比,**均为百分数原值**)。data.timestamp 为接口响应时间戳。',
1557
+ path: '/api/fund/portfolio/asset-allocation',
1558
+ inputSchema: objectSchema({
1559
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1560
+ }, ['thscode']),
1561
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundAssetAllocationFields) }),
1562
+ allowed: ['thscode'],
1563
+ normalize: onlyFundCode,
1564
+ guard: { item: fundAssetAllocationFields },
1565
+ },
1566
+ {
1567
+ name: 'get_fund_portfolio_industry_allocation',
1568
+ capability: 'fund_industry_allocation',
1569
+ summary: '基金行业配置比例(按报告期)',
1570
+ description: '获取单只基金按报告期的行业配置比例。thscode 必填带后缀。item[] 含 report_period(字符串报告期,形如 2026Q2)、industry_name(行业名称)与 ratio_pct(行业配置比例,百分数原值)。注意该数据并非所有基金都有:实测部分 ETF 返回 code=5003(上游数据源无该基金行业配置),这属于上游数据缺失而非参数错误,不要重试到超时。',
1571
+ path: '/api/fund/portfolio/industry-allocation',
1572
+ inputSchema: objectSchema({
1573
+ thscode: { type: 'string', description: '完整基金代码(如 025480.OF)' },
1574
+ }, ['thscode']),
1575
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundIndustryAllocationFields) }),
1576
+ allowed: ['thscode'],
1577
+ normalize: onlyFundCode,
1578
+ guard: { item: fundIndustryAllocationFields },
1579
+ },
1580
+ {
1581
+ name: 'get_fund_portfolio_stock_history',
1582
+ capability: 'fund_stock_history',
1583
+ summary: '基金历史股票持仓(按报告期与截止日)',
1584
+ description: '获取单只基金指定报告期的历史股票持仓。三个参数都必填:thscode(带后缀)、report_type(报告类型,**上游枚举未公开**,实测 quarter 有数据;annual/half 返回 5003 表示上游无该类型数据而非参数错误,因此不要据此判断参数对错)、end_date(报告截止日,格式 yyyy-MM-dd)。item[] 含持仓 thscode / ticker / name / asset_type(股票为 stock)/ hold_ratio(持仓占比,百分数原值)/ market_value(持仓市值,单位文档未给出)/ period_increase_pct(报告期增减比例,百分数原值)/ rank(**仅前十大返回 1~10,其余为 null**)/ report_type / end_date_ms。',
1585
+ path: '/api/fund/portfolio/stock-history',
1586
+ inputSchema: objectSchema({
1587
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1588
+ report_type: { type: 'string', description: '报告类型;上游未公开枚举,实测 quarter 可用' },
1589
+ end_date: { type: 'string', description: '报告截止日期,格式 yyyy-MM-dd' },
1590
+ }, ['thscode', 'report_type', 'end_date']),
1591
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundStockHistoryFields) }),
1592
+ allowed: ['thscode', 'report_type', 'end_date'],
1593
+ normalize: normalizeFundStockHistory,
1594
+ guard: { item: fundStockHistoryFields },
1595
+ },
1596
+ {
1597
+ name: 'get_fund_holders_detail',
1598
+ capability: 'fund_holders',
1599
+ summary: '基金持有人结构(机构/个人/员工占比)',
1600
+ description: '获取单只基金的持有人结构。thscode 必填带后缀;merge_scope 可选,取 all(默认,分别返回合并/独立份额的最新记录)/ merged(A/C 等份额合并披露)/ separate(当前份额独立披露)。item[] 含 merge_scope(实际口径,取值 merged 或 separate)、report_date_ms、ins_position(机构占比,百分数原值)、holder_amount(持有人户数)、avg_holder_share(平均每户持有份额,单位文档未给出)、psnl_rate(个人占比,百分数原值)、mgmt_staff_hold_rate(管理人员工持有比例,百分数原值)。merge_scope=all 时最多 2 条,单一口径最多 1 条;所选口径暂无数据返回 code=3002。data.timestamp 取返回记录中最新的报告日。',
1601
+ path: '/api/fund/holders/detail',
1602
+ inputSchema: objectSchema({
1603
+ thscode: { type: 'string', description: '完整基金代码(如 161725.SZ)' },
1604
+ merge_scope: { type: 'string', enum: MERGE_SCOPES, description: '披露口径:all 合并+独立(默认)/ merged 合并 / separate 独立' },
1605
+ }, ['thscode']),
1606
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundHolderFields) }),
1607
+ allowed: ['thscode', 'merge_scope'],
1608
+ normalize: normalizeFundHolders,
1609
+ guard: { item: fundHolderFields },
1610
+ },
1611
+ {
1612
+ name: 'get_fund_holders_top',
1613
+ capability: 'fund_top_holders',
1614
+ summary: '基金前十大持有人(含多报告期)',
1615
+ description: '获取基金前十大持有人名单。thscode 必填带后缀;limit 可选、取值 1~10(实测 0 / 11 / 201 均返回 1003)。**实测 limit 限定的是报告期数而不是返回行数**:limit=10 时返回 10 个报告期、共 99 行,因此引用条数时必须用 item 的实际长度,不要把 limit 当作行数;data.limit 是服务端实际采用的上限。item[] 含 holder_id / holder_code / holder_name / holder_type / rank / hold_share(持有份额,单位文档未给出)/ hold_rate_pct(持有比例,百分数原值)/ report_date_ms / publish_date_ms。',
1616
+ path: '/api/fund/holders/top',
1617
+ inputSchema: objectSchema({
1618
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1619
+ limit: { type: 'integer', minimum: 1, maximum: MAX_TOP_HOLDERS_LIMIT, description: '报告期数上限,取值 1~10;实测它限定报告期数而非返回行数' },
1620
+ }, ['thscode']),
1621
+ outputSchema: {
1622
+ type: 'object',
1623
+ properties: {
1624
+ timestamp: { type: 'integer' },
1625
+ limit: { type: 'integer' },
1626
+ item: { type: 'array', items: rowSchema(fundTopHolderFields) },
1627
+ },
1628
+ additionalProperties: true,
1629
+ },
1630
+ allowed: ['thscode', 'limit'],
1631
+ normalize: normalizeFundTopHolders,
1632
+ guard: { data: { limit: 'number?' }, item: fundTopHolderFields },
1633
+ },
1634
+ {
1635
+ name: 'get_fund_managers_detail',
1636
+ capability: 'fund_manager',
1637
+ summary: '基金经理详情(履历/年化/雷达对比)',
1638
+ description: '按经理 ID 获取基金经理详情。manager_id 必填,可从 fund_profile 的 manager_info[].manager_id 取得。item[] 含 manager_id / manager_name / sex / degree / company_id / company_name / resume(履历)/ photo_url / annual_return_pct / maximum_return_pct / radar_comparison(数组);除前两项外均可为 null。**口径警告**:文档对收益率单位自相矛盾(分组说明称百分数原值,官方响应示例却是小数形式 0.05096597),引用这两个字段时必须按原值转述并注明口径未确认,不要自行换算成百分比。',
1639
+ path: '/api/fund/managers/detail',
1640
+ inputSchema: objectSchema({
1641
+ manager_id: { type: 'string', description: '基金经理 ID,可从 fund_profile 的 manager_info[].manager_id 取得' },
1642
+ }, ['manager_id']),
1643
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundManagerFields) }),
1644
+ allowed: ['manager_id'],
1645
+ normalize: normalizeFundManager,
1646
+ guard: { item: fundManagerFields },
1647
+ },
1648
+ {
1649
+ name: 'get_fund_companies_detail',
1650
+ capability: 'fund_company',
1651
+ summary: '基金公司详情(规模/基金只数)',
1652
+ description: '按公司 ID 获取基金公司详情。company_id 必填,可从 fund_profile 的 company_id 取得。item[] 含 company_id / company_name / company_type / established_date_ms(成立日期,毫秒时间戳)/ fund_count(旗下基金数量)/ scale(管理规模)。**scale 的金额单位文档未给出**,按原值转述,不要擅自标成亿元。',
1653
+ path: '/api/fund/companies/detail',
1654
+ inputSchema: objectSchema({
1655
+ company_id: { type: 'string', description: '基金公司 ID,可从 fund_profile 的 company_id 取得' },
1656
+ }, ['company_id']),
1657
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundCompanyFields) }),
1658
+ allowed: ['company_id'],
1659
+ normalize: normalizeFundCompany,
1660
+ guard: { item: fundCompanyFields },
1661
+ },
1662
+ // ── Phase 3C:基金进阶子模块(19 个端点;已确证全部开放、非 AI 客户端专用)──
1663
+ // 口径提醒:基金各页**金额币种单位文档未给出**(对比 A 股财务页明写"原币元"),
1664
+ // 一律按原值转述;多处百分比为"百分数原值",但回测 metrics 口径文档自相矛盾。
1665
+ {
1666
+ name: 'get_fund_performance_indicators_historical',
1667
+ capability: 'fund_performance_history',
1668
+ summary: '基金净值指标历史(RSI/唐奇安/估值分位)',
1669
+ description: '获取基金按日的历史指标序列。thscode、start、end 三者都必填,start/end 为**毫秒级 Unix 时间戳**且 end >= start。item[] 含 date_ms 与三个指标:rsi_pct(净值波动 RSI)、donchian_channel(趋势强弱/唐奇安通道)、track_index_pe_ttm_five_year_percentile(跟踪指数 PE TTM 五年分位)。data.timestamp 保留明确的上游数据时间,不要当作响应时刻。',
1670
+ path: '/api/fund/performance/indicators-historical',
1671
+ inputSchema: objectSchema({
1672
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1673
+ start: { type: 'integer', minimum: 0, description: '起始毫秒级 Unix 时间戳(必填)' },
1674
+ end: { type: 'integer', minimum: 0, description: '结束毫秒级 Unix 时间戳(必填);end >= start' },
1675
+ }, ['thscode', 'start', 'end']),
1676
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundPerformanceHistoryFields) }),
1677
+ allowed: ['thscode', 'start', 'end'],
1678
+ normalize: normalizeFundPerformanceHistory,
1679
+ guard: { item: fundPerformanceHistoryFields },
1680
+ },
1681
+ {
1682
+ name: 'get_fund_portfolio_bond_history',
1683
+ capability: 'fund_bond_history',
1684
+ summary: '基金历史债券持仓(按报告期)',
1685
+ description: '获取单只基金指定报告期的历史债券持仓,字段结构与历史股票持仓一致:thscode / ticker / name / asset_type(债券为 bond)/ hold_ratio(百分数原值)/ market_value / period_increase_pct(百分数原值)/ report_type / end_date_ms;rank 仅前十大返回 1~10、其余为 null。三个参数都必填:thscode(带后缀)、report_type(上游枚举未公开,实测 quarter 可用)、end_date(格式 yyyy-MM-dd)。',
1686
+ path: '/api/fund/portfolio/bond-history',
1687
+ inputSchema: objectSchema({
1688
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1689
+ report_type: { type: 'string', description: '报告类型;上游未公开枚举,实测 quarter 可用' },
1690
+ end_date: { type: 'string', description: '报告截止日期,格式 yyyy-MM-dd' },
1691
+ }, ['thscode', 'report_type', 'end_date']),
1692
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundStockHistoryFields) }),
1693
+ allowed: ['thscode', 'report_type', 'end_date'],
1694
+ normalize: normalizeFundStockHistory,
1695
+ guard: { item: fundStockHistoryFields },
1696
+ },
1697
+ {
1698
+ name: 'get_fund_portfolio_stock_report_dates',
1699
+ capability: 'fund_stock_report_dates',
1700
+ summary: '基金股票持仓的可用报告期清单',
1701
+ description: '获取该基金股票持仓可用的报告期清单,用于在调用历史持仓前确定合法的 report_type 与 end_date 组合。thscode 必填;report_type **可选**(省略返回全部类型)。item[] 含 report_type、report_type_name(中文名,如"季度")与 start_date_ms / end_date_ms(该报告期的起止毫秒戳)。',
1702
+ path: '/api/fund/portfolio/stock-report-dates',
1703
+ inputSchema: objectSchema({
1704
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1705
+ report_type: { type: 'string', description: '报告类型,可选;省略返回全部类型' },
1706
+ }, ['thscode']),
1707
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundReportDatesFields) }),
1708
+ allowed: ['thscode', 'report_type'],
1709
+ normalize: normalizeFundReportType,
1710
+ guard: { item: fundReportDatesFields },
1711
+ },
1712
+ {
1713
+ name: 'get_fund_portfolio_bond_report_dates',
1714
+ capability: 'fund_bond_report_dates',
1715
+ summary: '基金债券持仓的可用报告期清单',
1716
+ description: '获取该基金债券持仓可用的报告期清单,字段结构与股票版一致(report_type / report_type_name / start_date_ms / end_date_ms)。thscode 必填;report_type 可选,省略返回全部类型。',
1717
+ path: '/api/fund/portfolio/bond-report-dates',
1718
+ inputSchema: objectSchema({
1719
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1720
+ report_type: { type: 'string', description: '报告类型,可选;省略返回全部类型' },
1721
+ }, ['thscode']),
1722
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundReportDatesFields) }),
1723
+ allowed: ['thscode', 'report_type'],
1724
+ normalize: normalizeFundReportType,
1725
+ guard: { item: fundReportDatesFields },
1726
+ },
1727
+ {
1728
+ name: 'get_fund_managers_experience',
1729
+ capability: 'fund_manager_experience',
1730
+ summary: '基金经理获奖、重仓与从业经历',
1731
+ description: '获取基金经理的获奖经历、代表性重仓与从业经历。manager_id 必填(可从 fund_profile 的 manager_info[].manager_id 取得)。item[] 含三个**保留上游结构**的字段:awards(可为数组、对象或 null)、heavy_assets(示例形如 {stock:[{trade_name, trade_code, market_value, scale}]})、investment_history(示例为以纯代码为键的 map,其 end 可以是"至今"这类字符串)。因此这些字段按原样转述,不要假设固定内部键名。',
1732
+ path: '/api/fund/managers/experience',
1733
+ inputSchema: objectSchema({
1734
+ manager_id: { type: 'string', description: '基金经理 ID,可从 fund_profile 的 manager_info[].manager_id 取得' },
1735
+ }, ['manager_id']),
1736
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundManagerExperienceFields) }),
1737
+ allowed: ['manager_id'],
1738
+ normalize: normalizeFundManager,
1739
+ guard: { item: fundManagerExperienceFields },
1740
+ },
1741
+ {
1742
+ name: 'get_fund_managers_investment_style',
1743
+ capability: 'fund_manager_style',
1744
+ summary: '基金经理投资风格(理念/行业偏好)',
1745
+ description: '获取基金经理的投资风格画像。manager_id 必填。item[] 含 representative_fund_thscode / representative_fund_ticker / representative_fund_name、investment_idea(投资理念)、total_fund_scale(管理基金总规模)、industry_preferences(行业偏好,**保留上游结构**,实测为年度记录数组,元素含 report_tag / percent[] / total_fund_scale)。除 industry_preferences 外所有字段都可为 null;关联基金解析失败时对应字段为空但不影响主能力。',
1746
+ path: '/api/fund/managers/investment-style',
1747
+ inputSchema: objectSchema({
1748
+ manager_id: { type: 'string', description: '基金经理 ID,可从 fund_profile 的 manager_info[].manager_id 取得' },
1749
+ }, ['manager_id']),
1750
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundManagerStyleFields) }),
1751
+ allowed: ['manager_id'],
1752
+ normalize: normalizeFundManager,
1753
+ guard: { item: fundManagerStyleFields },
1754
+ },
1755
+ {
1756
+ name: 'get_fund_managers_performance',
1757
+ capability: 'fund_manager_performance',
1758
+ summary: '基金经理区间收益与同类/基准对比',
1759
+ description: '获取基金经理在指定区间的收益表现。manager_id 与 range 都必填;**range 枚举只有 5 个值**:month / tmonth / year / nowyear / now(比 fund_nav 的 8 值枚举少)。item[] 含 date_ms 与 manager_return_pct(经理收益率)/ peer_return_pct(同类收益率)/ benchmark_return_pct(基准收益率),三者均为**百分数原值**。注意:若该经理缺少上游画像数据,接口会返回 code=5003 而非参数错误。',
1760
+ path: '/api/fund/managers/performance',
1761
+ inputSchema: objectSchema({
1762
+ manager_id: { type: 'string', description: '基金经理 ID' },
1763
+ range: { type: 'string', enum: MANAGER_RANGES, description: '区间:month / tmonth / year / nowyear / now' },
1764
+ }, ['manager_id', 'range']),
1765
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundManagerPerformanceFields) }),
1766
+ allowed: ['manager_id', 'range'],
1767
+ normalize: normalizeManagerRange,
1768
+ guard: { item: fundManagerPerformanceFields },
1769
+ },
1770
+ {
1771
+ name: 'get_fund_financials_income_statements',
1772
+ capability: 'fund_income',
1773
+ summary: '基金利润表(按报告期)',
1774
+ description: '获取基金定期报告的利润表序列。thscode 必填带后缀。item[] 含起止与披露日期(start_date_ms / end_date_ms / publish_date_ms)以及收入与费用类金额字段:investment_income、stock/bond/fund_investment_income、dividend_income、interest_income、fair_value_income、exchange_income、other_income、total_income、fee、total_fee、manager_reward、custodian_fee、transaction_cost、tax_surcharge、total_profit、net_profit 等。**未披露字段保持 null,不补零**;金额的币种单位文档未给出(对比 A 股财务页明写"原币元"),按原值转述。',
1775
+ path: '/api/fund/financials/income-statements',
1776
+ inputSchema: objectSchema({
1777
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1778
+ }, ['thscode']),
1779
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundIncomeFields) }),
1780
+ allowed: ['thscode'],
1781
+ normalize: onlyFundCode,
1782
+ guard: { item: fundIncomeFields },
1783
+ },
1784
+ {
1785
+ name: 'get_fund_financials_balance_sheets',
1786
+ capability: 'fund_balance',
1787
+ summary: '基金资产负债表(按报告期)',
1788
+ description: '获取基金定期报告的资产负债表序列。thscode 必填带后缀。item[] 含三个日期字段与资产/负债/权益类字段:total_assets、bank_deposit、fund_investment、stock_investment、bond_investment、transactional_financial_assets、other_assets、total_liability、other_liability、owner_total_equity、undistributed_profit、liability_and_owner_equity。未披露字段为 null;金额单位文档未给出,按原值转述。',
1789
+ path: '/api/fund/financials/balance-sheets',
1790
+ inputSchema: objectSchema({
1791
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1792
+ }, ['thscode']),
1793
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundBalanceFields) }),
1794
+ allowed: ['thscode'],
1795
+ normalize: onlyFundCode,
1796
+ guard: { item: fundBalanceFields },
1797
+ },
1798
+ {
1799
+ name: 'get_fund_financials_indicators',
1800
+ capability: 'fund_financial_indicators',
1801
+ summary: '基金财务指标(净值/利润/增长率)',
1802
+ description: '获取基金定期报告的财务指标序列。thscode 必填带后缀。item[] 含三个日期字段与指标:distribution_profit(本期已实现收益)、current_profit(本期利润)、current_income、distribution_share_profit(每份可分配利润)、average_nav_profit_margin(平均净值利润率)、average_share_current_profit(平均每份本期利润)、share_nav(单位净值)、sum_share_nav(累计单位净值)、asset_nav(基金资产净值)、sum_nav_rate / nav_rate(累计/净值增长率)。这些字段**文档未标注是百分数还是小数形式**(只有基金级总则),引用增长率与净值时按原值转述并注明口径。',
1803
+ path: '/api/fund/financials/indicators',
1804
+ inputSchema: objectSchema({
1805
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1806
+ }, ['thscode']),
1807
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundFinancialIndicatorFields) }),
1808
+ allowed: ['thscode'],
1809
+ normalize: onlyFundCode,
1810
+ guard: { item: fundFinancialIndicatorFields },
1811
+ },
1812
+ {
1813
+ name: 'get_fund_indicators_line',
1814
+ capability: 'fund_indicator_line',
1815
+ summary: '基金指标画线序列(多指标多标的)',
1816
+ description: '按指标分组批量取画线序列。两个参数都是 **JSON 字符串**且都必填:indexes 为**数组**,每组必须含完整代码数组 thscodes 与指标数组 index_info(每项必含字符串 index_id,可选 attribute);time_range 为**对象**,必含字符串 time_type,可选 start / end(Unix 毫秒整数)与 offset(整数周期偏移)。响应是三层的:time_range 为毫秒时间轴数组(元素可为 null、与每组 values 按位置对应)、indexes 为指标元信息(index_id / value_type / timestamp / time_type / attribute,其中 timestamp 为正数时是毫秒戳、0 与负数是位置选择)、data[] 为按代码组织的序列(thscode + values[],每项含 idx 指向 indexes 下标与 values 数组)。',
1817
+ path: '/api/fund/indicators/line',
1818
+ inputSchema: objectSchema({
1819
+ indexes: { type: 'string', description: 'JSON 数组字符串:[{thscodes:[完整代码], index_info:[{index_id, attribute?}]}]' },
1820
+ time_range: { type: 'string', description: 'JSON 对象字符串:{time_type, start?, end?, offset?}(start/end 为 Unix 毫秒整数)' },
1821
+ }, ['indexes', 'time_range']),
1822
+ outputSchema: {
1823
+ type: 'object',
1824
+ properties: {
1825
+ time_range: { type: 'array', items: { oneOf: [{ type: 'integer' }, { type: 'null' }] } },
1826
+ indexes: { type: 'array', items: { type: 'object', additionalProperties: true } },
1827
+ data: { type: 'array', items: { type: 'object', additionalProperties: true } },
1828
+ },
1829
+ additionalProperties: true,
1830
+ },
1831
+ allowed: ['indexes', 'time_range'],
1832
+ normalize: normalizeFundIndicatorLine,
1833
+ guard: { custom: validateIndicatorLine },
1834
+ },
1835
+ {
1836
+ name: 'get_fund_indicators_table',
1837
+ capability: 'fund_indicator_table',
1838
+ summary: '基金指标表格查询(分页/排序)',
1839
+ description: '按选择器查询指标表格。四个参数都是 **JSON 字符串且全部可选**:code_selectors(对象;stock_code / fund_code 类型用 thscodes 数组,其它实体类型用 values)、indexes(数组,每项必含 index_id,可选 timestamp:正数=Unix 毫秒、0=最新位置、负数=位置偏移)、page_info(对象,page_begin / page_size / code_begin / code_page_size 均可选整数,**起点为 0**)、sort(数组,每项含整数 idx 指标下标与字符串 type 排序方向)。**该端点没有业务默认值,四个参数至少要给一个**。⚠️ 实测(2026-09)本端点对多只基金均返回 code=5003 数据源不可用(参数本身被接受、不是参数错误),属上游数据源缺口;接入时按 5003 如实回传,不要反复重试。响应含 total(上游结果总数)、indexes(与画线接口语义一致)、data[](thscode + values[],每项含 idx 与 value——value 可为 null,数值字符串保持原样如 "3.127400")与可空的 part_order_thscodes。',
1840
+ path: '/api/fund/indicators/table',
1841
+ inputSchema: objectSchema({
1842
+ code_selectors: { type: 'string', description: 'JSON 对象字符串:选择器(如 {fund_code:{thscodes:[...]}})' },
1843
+ indexes: { type: 'string', description: 'JSON 数组字符串:[{index_id, timestamp?}]' },
1844
+ page_info: { type: 'string', description: 'JSON 对象字符串:{page_begin?, page_size?, code_begin?, code_page_size?}(起点 0)' },
1845
+ sort: { type: 'string', description: 'JSON 数组字符串:[{idx, type}]' },
1846
+ }),
1847
+ outputSchema: {
1848
+ type: 'object',
1849
+ properties: {
1850
+ total: { type: 'integer' },
1851
+ indexes: { type: 'array', items: { type: 'object', additionalProperties: true } },
1852
+ data: { type: 'array', items: { type: 'object', additionalProperties: true } },
1853
+ part_order_thscodes: { oneOf: [{ type: 'array', items: { oneOf: [{ type: 'string' }, { type: 'null' }] } }, { type: 'null' }] },
1854
+ },
1855
+ additionalProperties: true,
1856
+ },
1857
+ allowed: ['code_selectors', 'indexes', 'page_info', 'sort'],
1858
+ normalize: normalizeFundIndicatorTable,
1859
+ guard: { custom: validateIndicatorTable },
1860
+ paginated: true,
1861
+ },
1862
+ {
1863
+ name: 'get_fund_corporate_actions_dividends',
1864
+ capability: 'fund_dividends',
1865
+ summary: '基金分红记录(每 10 份口径)',
1866
+ description: '获取基金的历史分红记录。thscode 必填带后缀。data 顶层除 item 外还有两个元数据字段:dividend_count(分红记录总数)与 dividend_total(服务端返回的**累计分红汇总值,不要与每 10 份现金分红混用**)。item[] 含 per_ten_cash_before_tax / per_ten_cash_after_tax(**每 10 份**税前/税后现金分红)、progress(进度)、以及六个日期毫秒戳:publish_date_ms、registration_date_ms、ex_dividend_date_ms、payment_date_ms、reinvestment_date_ms、profit_base_date_ms、in_dividend_date_ms。',
1867
+ path: '/api/fund/corporate-actions/dividends',
1868
+ inputSchema: objectSchema({
1869
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1870
+ }, ['thscode']),
1871
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundDividendFields) }),
1872
+ allowed: ['thscode'],
1873
+ normalize: onlyFundCode,
1874
+ guard: { data: { dividend_count: 'number?', dividend_total: 'number?' }, item: fundDividendFields },
1875
+ },
1876
+ {
1877
+ name: 'get_fund_offerings_list',
1878
+ capability: 'fund_offerings',
1879
+ summary: '基金募集列表(当前/即将募集)',
1880
+ description: '获取正在或即将募集的基金列表。subscribe 必填,只接受 active(当前募集)或 upcoming(即将募集)。item[] 含 thscode、ticker 与 subscription_start_ms / subscription_end_ms(募集起止毫秒时间戳)。',
1881
+ path: '/api/fund/offerings/list',
1882
+ inputSchema: objectSchema({
1883
+ subscribe: { type: 'string', enum: OFFERING_SUBSCRIBE, description: 'active 当前募集 / upcoming 即将募集' },
1884
+ }, ['subscribe']),
1885
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundOfferingFields) }),
1886
+ allowed: ['subscribe'],
1887
+ normalize: (params) => ({ subscribe: requiredEnum(params.subscribe, 'subscribe', OFFERING_SUBSCRIBE, lowerCase) }),
1888
+ guard: { item: fundOfferingFields },
1889
+ },
1890
+ {
1891
+ name: 'get_fund_quota_list',
1892
+ capability: 'fund_quota_list',
1893
+ summary: 'QDII 额度分类与基金明细(三层嵌套)',
1894
+ description: '获取 QDII 额度分类下的基金明细。tab 必填,是 **URL 编码的 JSON 数组字符串**(分类值由上游判定,未知分类按正常空结果返回);buy 可选布尔(可购状态过滤,省略时不向上游注入默认值)。响应是**三层嵌套且没有 item 信封**:data[] 为分类(name + sub_tab[]),sub_tab[] 为子分类(name + fund_list[]),fund_list[] 为基金(thscode、fund_name、quota、year)。quota 为 null 表示**无限额**;year 是上游提供的近一年收益率**字符串,不作单位换算**;分类/子分类/基金列表中的合法 null 占位保持原义。',
1895
+ path: '/api/fund/quota/list',
1896
+ inputSchema: objectSchema({
1897
+ tab: { type: 'string', description: 'JSON 数组字符串,例如 ["remen"];分类值由上游判定' },
1898
+ buy: { type: 'boolean', description: '可购状态过滤,可选;省略不注入默认值' },
1899
+ }, ['tab']),
1900
+ outputSchema: {
1901
+ type: 'array',
1902
+ items: {
1903
+ type: 'object',
1904
+ properties: {
1905
+ name: { type: 'string' },
1906
+ sub_tab: {
1907
+ type: 'array',
1908
+ items: {
1909
+ type: 'object',
1910
+ properties: {
1911
+ name: { type: 'string' },
1912
+ fund_list: {
1913
+ type: 'array',
1914
+ items: {
1915
+ type: 'object',
1916
+ properties: { thscode: { type: 'string' }, fund_name: { type: 'string' }, quota: { oneOf: [{ type: 'string' }, { type: 'null' }] }, year: { oneOf: [{ type: 'string' }, { type: 'null' }] } },
1917
+ additionalProperties: true,
1918
+ },
1919
+ },
1920
+ },
1921
+ additionalProperties: true,
1922
+ },
1923
+ },
1924
+ },
1925
+ additionalProperties: true,
1926
+ },
1927
+ },
1928
+ allowed: ['tab', 'buy'],
1929
+ normalize: normalizeFundQuotaList,
1930
+ guard: { custom: validateQuotaList },
1931
+ },
1932
+ {
1933
+ name: 'get_fund_quota_summary',
1934
+ capability: 'fund_quota_summary',
1935
+ summary: 'QDII 额度分类汇总',
1936
+ description: '获取 QDII 额度按分类的汇总。tab 必填,是 **URL 编码的 JSON 数组字符串**(示例 ["nazhi100"])。响应是**扁平数组 data[]**(没有 item 信封):每项含 name(分类名称)、unlimited(无额度限制的基金数)、total_limit(限额合计)、total(基金总数)、buy(可购基金数)——**这五个字段全部以字符串原样返回,不做单位转换**。',
1937
+ path: '/api/fund/quota/summary',
1938
+ inputSchema: objectSchema({
1939
+ tab: { type: 'string', description: 'JSON 数组字符串,例如 ["nazhi100"]' },
1940
+ }, ['tab']),
1941
+ outputSchema: { type: 'array', items: rowSchema(fundQuotaSummaryFields) },
1942
+ allowed: ['tab'],
1943
+ normalize: (params) => ({ tab: jsonParam(params.tab, 'tab', 'array') }),
1944
+ guard: { dataArray: fundQuotaSummaryFields },
1945
+ },
1946
+ {
1947
+ name: 'get_fund_diagnostics_detail',
1948
+ capability: 'fund_diagnostics',
1949
+ summary: '基金诊断(多维画像与同类对比)',
1950
+ description: '获取单只基金的诊断画像。thscode 必填带后缀。item[] 含 thscode / ticker / fund_type / peer_code(同类比较分组代码,如 ETF-INDEX)与六个**保留上游结构**的对象字段:dimensions、peer_dimensions、probabilities、ranges、resilience、peer_resilience(官方示例中均为空对象,内部键名文档未给出)。因此这些对象按原样转述,不要假设固定内部键名。',
1951
+ path: '/api/fund/diagnostics/detail',
1952
+ inputSchema: objectSchema({
1953
+ thscode: { type: 'string', description: '完整基金代码(如 510300.SH)' },
1954
+ }, ['thscode']),
1955
+ outputSchema: envelopeData({ type: 'array', items: rowSchema(fundDiagnosticsFields) }),
1956
+ allowed: ['thscode'],
1957
+ normalize: onlyFundCode,
1958
+ guard: { item: fundDiagnosticsFields },
1959
+ },
1960
+ {
1961
+ name: 'get_fund_backtest_result',
1962
+ capability: 'fund_backtest',
1963
+ summary: '基金在线回测(策略/基准/曲线)',
1964
+ description: '对单只基金执行在线回测。六个参数全部必填:thscode(带后缀);buy_conditions 与 sell_conditions 为 **JSON 字符串且必须是对象或数组**;buy_frequency_type 为买入频率(**具体取值由上游判定,无公开枚举**,示例 WEEKLY);max_buy_times 为最大买入次数(整数 >= 1);per_buy_amount 为每次买入金额。响应含 start_date / end_date(**yyyy-MM-dd 字符串,不是毫秒**)、metrics(strategy_return / fund_return / excess_return / win_rate / max_drawdown)、trade_count、trades[](trade_date、type、price、amount、shares、profit——profit 可为 null)与 curve_points(**兼容数组或对象结构**)。**口径警告**:回测 metrics 的单位文档自相矛盾(总览称百分数原值,回测页称"不进行单位换算",示例值形如小数 0.4224),引用时必须按原值转述并注明口径未确认,不要自行 ×100。',
1965
+ path: '/api/fund/backtest/result',
1966
+ inputSchema: objectSchema({
1967
+ thscode: { type: 'string', description: '完整基金代码(如 000001.OF)' },
1968
+ buy_conditions: { type: 'string', description: 'JSON 字符串(对象或数组),如 {"indicator_code":"rsi_pct","operator":">","value":0.5}' },
1969
+ sell_conditions: { type: 'string', description: 'JSON 字符串(对象或数组)' },
1970
+ buy_frequency_type: { type: 'string', description: '买入频率,取值由上游判定(示例 WEEKLY)' },
1971
+ max_buy_times: { type: 'integer', minimum: 1, description: '最大买入次数' },
1972
+ per_buy_amount: { type: 'number', minimum: 0, description: '每次买入金额' },
1973
+ }, ['thscode', 'buy_conditions', 'sell_conditions', 'buy_frequency_type', 'max_buy_times', 'per_buy_amount']),
1974
+ outputSchema: {
1975
+ type: 'object',
1976
+ properties: {
1977
+ start_date: { type: 'string' },
1978
+ end_date: { type: 'string' },
1979
+ metrics: { type: 'object', additionalProperties: true },
1980
+ trade_count: { type: 'number' },
1981
+ trades: { type: 'array', items: { type: 'object', additionalProperties: true } },
1982
+ curve_points: {},
1983
+ },
1984
+ additionalProperties: true,
1985
+ },
1986
+ allowed: ['thscode', 'buy_conditions', 'sell_conditions', 'buy_frequency_type', 'max_buy_times', 'per_buy_amount'],
1987
+ normalize: normalizeFundBacktest,
1988
+ guard: { data: { start_date: 'text', end_date: 'text', metrics: 'object', trade_count: 'number?', trades: 'array' } },
1989
+ },
1990
+ {
1991
+ name: 'get_fund_backtest_indicators',
1992
+ capability: 'fund_backtest_indicators',
1993
+ summary: '回测可用指标清单(无参数)',
1994
+ description: '获取回测支持的指标清单,无业务参数(仅凭据)。响应是**扁平数组 data[]**(没有 item 信封):每项含 id、indicator_name、indicator_code(如 rsi_pct)、type 与 value_kind(上游定义)、support_operation(支持的运算符,如 ">,<")、unit / description / state_rules(均可为 null)与 create_time / update_time(**ISO 本地日期时间字符串**,如 2026-05-27T16:57:32,不是毫秒时间戳)。用 buy_conditions 前应先查这里的 indicator_code 与 support_operation。',
1995
+ path: '/api/fund/backtest/indicators',
1996
+ inputSchema: objectSchema({}),
1997
+ outputSchema: { type: 'array', items: rowSchema(fundBacktestIndicatorFields) },
1998
+ allowed: [],
1999
+ normalize: () => ({}),
2000
+ guard: { dataArray: fundBacktestIndicatorFields },
2001
+ },
2002
+ ];
2003
+ }
2004
+ function createSource(definition, baseUrl, resolveApiKey) {
2005
+ const { name, capability, summary, description, path, inputSchema, outputSchema, allowed } = definition;
2006
+ const schema = {
2007
+ capability,
2008
+ time_contract: getDataTimeContract(capability),
2009
+ name,
2010
+ source: 'api:fuyao',
2011
+ // 内部数据源身份证(provider.kind.resource):宿主路由/审计用,不进入模型协议。
2012
+ data_key: buildDataKey('fuyao', 'api', path),
2013
+ source_label: 'fuyao',
2014
+ paginated: definition.paginated === true,
2015
+ cacheMaxAgeMs: definition.cacheMaxAgeMs,
2016
+ // 行数组位置由端点自己声明(护栏里的 itemKey 就是同一份知识):龙虎榜的行
2017
+ // 在 `stock_items` 下,不声明就会被存储层按 `item` 猜成「非行集合」。
2018
+ rowShape: { rowKey: definition.guard.itemKey ?? 'item', ...(definition.guard.rowKeys === undefined ? {} : { rowKeys: definition.guard.rowKeys }) },
2019
+ summary,
2020
+ description,
2021
+ input_schema: inputSchema,
2022
+ output_schema: outputSchema,
2023
+ };
2024
+ /** 幂等:Hub 入队前调用一次(digest 口径),execute 再调用一次(直连调用也要生效)。 */
2025
+ const normalize = (params) => {
2026
+ const values = paramsObject(params);
2027
+ assertKnownParameters(values, allowed);
2028
+ return definition.normalize(values);
2029
+ };
2030
+ return {
2031
+ schema,
2032
+ normalizeParams: normalize,
2033
+ validateOutput: buildGuard(definition.guard),
2034
+ execute: async (request, signal) => {
2035
+ const params = normalize(request.params);
2036
+ const apiKey = await resolveApiKey();
2037
+ if (!apiKey)
2038
+ throw new Error(`FUYAO_API_KEY is not configured; data source ${name} is unavailable`);
2039
+ return callFuyao(baseUrl, apiKey, path, fuyaoQuery(params, allowed), signal, outputSchema);
2040
+ },
2041
+ };
2042
+ }
2043
+ export function createFuyaoRestSources(resolveApiKey, baseUrl = DEFAULT_BASE_URL) {
2044
+ const cleanBaseUrl = baseUrl.replace(/\/$/, '');
2045
+ return endpointDefinitions().map((definition) => createSource(definition, cleanBaseUrl, resolveApiKey));
2046
+ }
2047
+ export async function resolveFuyaoApiKey(ctx, credentialRef = 'FUYAO_API_KEY') {
2048
+ const credentials = ctx.get('credentials');
2049
+ if (credentials) {
2050
+ const resolved = await credentials.resolve(credentialRef);
2051
+ if (resolved?.value)
2052
+ return resolved.value;
2053
+ }
2054
+ const processLike = globalThis.process;
2055
+ return processLike?.env?.[credentialRef];
2056
+ }
2057
+ export { MAX_HISTORY_WINDOW_MS, MAX_BATCH_CODES };