@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,1253 @@
1
+ import { buildDataKey } from '../data-collector/hub.js';
2
+ import { getDataTimeContract } from '../time/tools.js';
3
+ import { sharedEastmoneyClient, EastmoneyTransportError } from '../net/eastmoney-client.js';
4
+ import { normalizeEastmoneyAshareIdentity, normalizeEastmoneyBoardIdentity, normalizeEastmoneySecurityIdentity } from './security-identity.js';
5
+ const DATACENTER_URL = 'https://datacenter-web.eastmoney.com/api/data/v1/get';
6
+ const CLIST_URL = 'https://push2.eastmoney.com/api/qt/clist/get';
7
+ const MAX_PAGE_SIZE = 500;
8
+ const EASTMONEY_UT = 'bd1d9ddb04089700cf9c27f6f7426281';
9
+ const BOARD_TYPES = ['industry', 'concept', 'region'];
10
+ const BOARD_FS = { industry: 'm:90+t:2', concept: 'm:90+t:3', region: 'm:90+t:1' };
11
+ const SORT_FIELDS = { change_pct: 'f3', main_net_inflow: 'f62', amount: 'f6' };
12
+ const topBuySellRow = {
13
+ type: 'object',
14
+ properties: {
15
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' }, market: { type: 'string' },
16
+ trade_date: { type: 'string' }, trade_date_ms: { type: 'integer' }, close_price: { oneOf: [{ type: 'number' }, { type: 'null' }] },
17
+ change_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] }, turnover_rate_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
18
+ billboard_buy_amt: { oneOf: [{ type: 'number' }, { type: 'null' }] }, billboard_sell_amt: { oneOf: [{ type: 'number' }, { type: 'null' }] },
19
+ billboard_net_amt: { oneOf: [{ type: 'number' }, { type: 'null' }] }, billboard_deal_amt: { oneOf: [{ type: 'number' }, { type: 'null' }] },
20
+ deal_amount_ratio_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] }, deal_net_ratio_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
21
+ market_deal_amt: { oneOf: [{ type: 'number' }, { type: 'null' }] }, free_market_cap: { oneOf: [{ type: 'number' }, { type: 'null' }] },
22
+ explanation: { oneOf: [{ type: 'string' }, { type: 'null' }] }, explain: { oneOf: [{ type: 'string' }, { type: 'null' }] },
23
+ trade_market: { oneOf: [{ type: 'string' }, { type: 'null' }] }, trade_id: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
24
+ eastmoney_code: { type: 'string' }, eastmoney_market: { type: 'string' },
25
+ },
26
+ additionalProperties: true,
27
+ };
28
+ const datacenterOutput = (row) => ({
29
+ type: 'object',
30
+ properties: {
31
+ item: { type: 'array', items: row },
32
+ pagination: {
33
+ type: 'object',
34
+ properties: { page: { type: 'integer' }, size: { type: 'integer' }, pages: { type: 'integer' }, total: { type: 'integer' } },
35
+ required: ['page', 'size', 'pages', 'total'], additionalProperties: false,
36
+ },
37
+ },
38
+ required: ['item', 'pagination'],
39
+ additionalProperties: true,
40
+ });
41
+ const lockupRow = {
42
+ type: 'object',
43
+ properties: {
44
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' },
45
+ free_date: { type: 'string' }, free_date_ms: { type: 'integer' }, free_shares_type: { oneOf: [{ type: 'string' }, { type: 'null' }] },
46
+ free_shares_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] }, total_ratio: { oneOf: [{ type: 'number' }, { type: 'null' }] },
47
+ non_free_shares_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] }, able_free_shares_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
48
+ eastmoney_code: { type: 'string' }, eastmoney_market: { type: 'string' },
49
+ },
50
+ additionalProperties: true,
51
+ };
52
+ const sectorRow = {
53
+ type: 'object',
54
+ properties: {
55
+ board_code: { type: 'string' }, board_name: { type: 'string' }, board_type: { enum: [...BOARD_TYPES] },
56
+ last_price: { oneOf: [{ type: 'number' }, { type: 'null' }] }, change_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
57
+ change_amount: { oneOf: [{ type: 'number' }, { type: 'null' }] }, volume: { oneOf: [{ type: 'number' }, { type: 'null' }] },
58
+ amount: { oneOf: [{ type: 'number' }, { type: 'null' }] }, amplitude_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
59
+ turnover_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] }, pe: { oneOf: [{ type: 'number' }, { type: 'null' }] }, pb: { oneOf: [{ type: 'number' }, { type: 'null' }] },
60
+ eastmoney_server_time: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
61
+ },
62
+ additionalProperties: true,
63
+ };
64
+ const cashflowRow = {
65
+ ...sectorRow,
66
+ properties: {
67
+ ...sectorRow.properties,
68
+ main_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] }, main_net_inflow_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
69
+ super_large_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] }, super_large_net_inflow_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
70
+ large_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] }, large_net_inflow_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
71
+ medium_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] }, medium_net_inflow_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
72
+ small_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] }, small_net_inflow_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
73
+ },
74
+ };
75
+ function isRecord(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
76
+ function assertKnown(params, allowed) { for (const key of Object.keys(params))
77
+ if (!allowed.includes(key))
78
+ throw new Error(`unsupported parameter: ${key}`); }
79
+ function numberOrNull(value) {
80
+ if (value === undefined || value === null || value === '')
81
+ return null;
82
+ const parsed = typeof value === 'number' ? value : Number(String(value).replaceAll(',', '').trim());
83
+ return Number.isFinite(parsed) ? parsed : null;
84
+ }
85
+ function integer(value, name, min, max) {
86
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max)
87
+ throw new Error(`${name} must be an integer from ${min} to ${max}`);
88
+ return value;
89
+ }
90
+ function date(value, name) {
91
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
92
+ throw new Error(`${name} must be YYYY-MM-DD`);
93
+ const parsed = new Date(`${value}T00:00:00Z`);
94
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value)
95
+ throw new Error(`${name} must be a valid YYYY-MM-DD date`);
96
+ return value;
97
+ }
98
+ function dateMs(value) { return Date.parse(`${value}T00:00:00+08:00`); }
99
+ function textOrNull(value) { return value === undefined || value === null || value === '' ? null : String(value); }
100
+ function sourceError(message, code = 'eastmoney_source_error') { const error = new Error(message); error.code = code; return error; }
101
+ /**
102
+ * 东财请求统一走**进程级共享**的东财客户端(`src/net/eastmoney-client.ts`)。
103
+ *
104
+ * 为什么必须共享:东财按**出口 IP** 风控(社区实测 >5 次/秒、1 分钟 ≥200 次、5 分钟 ≥300 次
105
+ * 即临时封禁),而本仓曾经有**两份互不知情**的东财出口——数据面这里的裸 `fetch`,与
106
+ * web_retriever 具名来源工具自己的一份。两份独立限流等于没有限流。
107
+ * 现在两条数据面共用同一个节流器:数据面按"可复用性"分层(数值/分页走 Hub、流式文本走
108
+ * web_retriever),但**网络面只有一份**。
109
+ *
110
+ * 本函数原有的错误码语义(`eastmoney_rate_limit` / `eastmoney_http_error` /
111
+ * `eastmoney_invalid_response`)刻意保持不变——收敛的是网络面,不是调用方的契约。
112
+ */
113
+ async function getJson(url, signal, context) {
114
+ let payload;
115
+ try {
116
+ payload = await sharedEastmoneyClient().fetchJson(url, { signal });
117
+ }
118
+ catch (error) {
119
+ // 调用方取消原样抛出;上游 HTTP 错误状态按既有口径归类。
120
+ if (error instanceof Error && error.name === 'AbortError')
121
+ throw error;
122
+ const status = error instanceof EastmoneyTransportError ? error.status : undefined;
123
+ throw sourceError(status === undefined
124
+ ? `Eastmoney request failed for ${context}: ${error instanceof Error ? error.message : String(error)}`
125
+ : `Eastmoney HTTP ${status} for ${context}`, status === 429 ? 'eastmoney_rate_limit' : 'eastmoney_http_error');
126
+ }
127
+ if (!isRecord(payload))
128
+ throw sourceError(`Eastmoney ${context} returned a non-object response`, 'eastmoney_invalid_response');
129
+ return payload;
130
+ }
131
+ function requireDatacenterResult(payload, context) {
132
+ // 上游用 code 9201 表示"返回数据为空"(success:false),这是**真实数据缺口**而非上游故障:
133
+ // 节假日、或该窗口确实没有任何记录。归类成 eastmoney_upstream_error 会诱导模型反复重试
134
+ // (§10.4 明令"数据缺口不要重试"),也与单票路径把 9201 判成"无记录"自相矛盾。
135
+ if (payload.success !== true && Number(payload.code) === 9201)
136
+ return { data: [], pages: 0, count: 0 };
137
+ if (payload.success !== true || Number(payload.code) !== 0 || !isRecord(payload.result))
138
+ throw sourceError(`Eastmoney ${context} failed: ${String(payload.message ?? 'unknown upstream error')} (code ${String(payload.code ?? 'unknown')})`, 'eastmoney_upstream_error');
139
+ return payload.result;
140
+ }
141
+ function requireRows(result, context) {
142
+ if (!Array.isArray(result.data) || typeof result.pages !== 'number' || typeof result.count !== 'number')
143
+ throw sourceError(`Eastmoney ${context} result shape changed`, 'eastmoney_invalid_response');
144
+ if (!result.data.every(isRecord))
145
+ throw sourceError(`Eastmoney ${context} contains a non-object row`, 'eastmoney_invalid_response');
146
+ return { rows: result.data, pages: result.pages, total: result.count };
147
+ }
148
+ function pagination(page, size, pages, total) { return { page, size, pages, total }; }
149
+ async function eastmoneyTickerExists(ticker, signal) {
150
+ const identity = normalizeEastmoneySecurityIdentity({ secucode: ticker });
151
+ const marketId = identity.eastmoney_market === 'SH' ? '1' : '0';
152
+ const url = new URL('https://push2.eastmoney.com/api/qt/stock/get');
153
+ url.searchParams.set('secid', `${marketId}.${identity.ticker}`);
154
+ url.searchParams.set('fields', 'f57,f58,f107');
155
+ const payload = await getJson(url.toString(), signal, 'ticker metadata');
156
+ return Number(payload.rc) === 0 && isRecord(payload.data)
157
+ && String(payload.data.f57 ?? '') === identity.ticker
158
+ && typeof payload.data.f58 === 'string'
159
+ && String(payload.data.f58).trim().length > 0;
160
+ }
161
+ function normalizeDateRange(params, allowed) {
162
+ assertKnown(params, allowed);
163
+ const start_date = date(params.start_date, 'start_date');
164
+ const end_date = date(params.end_date, 'end_date');
165
+ if (start_date > end_date)
166
+ throw new Error('start_date must not be later than end_date');
167
+ const page = params.page === undefined ? 1 : integer(params.page, 'page', 1, Number.MAX_SAFE_INTEGER);
168
+ const size = params.size === undefined ? 100 : integer(params.size, 'size', 1, MAX_PAGE_SIZE);
169
+ return { start_date, end_date, page, size };
170
+ }
171
+ function normalizeTicker(params) {
172
+ assertKnown(params, ['start_date', 'end_date', 'ticker', 'page', 'size']);
173
+ const normalized = normalizeDateRange(params, ['start_date', 'end_date', 'page', 'size', 'ticker']);
174
+ const identity = normalizeEastmoneySecurityIdentity({ secucode: params.ticker });
175
+ return { ...normalized, ticker: identity.thscode };
176
+ }
177
+ function normalizeBoard(params) {
178
+ assertKnown(params, ['board_type', 'sort_field', 'page', 'size']);
179
+ const board_type = params.board_type === undefined ? 'industry' : String(params.board_type).toLowerCase();
180
+ if (!BOARD_TYPES.includes(board_type))
181
+ throw new Error(`board_type must be one of ${BOARD_TYPES.join(', ')}`);
182
+ const sort_field = params.sort_field === undefined ? 'change_pct' : String(params.sort_field).toLowerCase();
183
+ if (!(sort_field in SORT_FIELDS))
184
+ throw new Error(`sort_field must be one of ${Object.keys(SORT_FIELDS).join(', ')}`);
185
+ const page = params.page === undefined ? 1 : integer(params.page, 'page', 1, Number.MAX_SAFE_INTEGER);
186
+ const size = params.size === undefined ? 100 : integer(params.size, 'size', 1, MAX_PAGE_SIZE);
187
+ return { board_type, sort_field, page, size };
188
+ }
189
+ function normalizeCashflowBoard(params) {
190
+ assertKnown(params, ['board_type', 'page', 'size']);
191
+ const board_type = params.board_type === undefined ? 'industry' : String(params.board_type).toLowerCase();
192
+ if (!BOARD_TYPES.includes(board_type))
193
+ throw new Error(`board_type must be one of ${BOARD_TYPES.join(', ')}`);
194
+ const page = params.page === undefined ? 1 : integer(params.page, 'page', 1, Number.MAX_SAFE_INTEGER);
195
+ const size = params.size === undefined ? 100 : integer(params.size, 'size', 1, MAX_PAGE_SIZE);
196
+ return { board_type, page, size };
197
+ }
198
+ function topFilter(params, ticker) {
199
+ const base = `(TRADE_DATE<='${params.end_date}')(TRADE_DATE>='${params.start_date}')`;
200
+ const code = ticker?.replace(/\.(SH|SZ|BJ)$/i, '');
201
+ return code ? `${base}(SECURITY_CODE="${code}")` : base;
202
+ }
203
+ function parseTopRow(raw) {
204
+ // ⚠️ 上游 `MARKET` 是自由文本(实测存在 `SZ` 之外的写法),而 SECUCODE 自带市场后缀时
205
+ // 它是冗余信息:只有 SECUCODE 不带市场时才拿它做必填校验,否则原文喂进硬白名单会把
206
+ // 整页龙虎榜判成错误(护栏误杀真实数据,§10.2)。行里的 `market` 字段仍原样保留。
207
+ const secucode = typeof raw.SECUCODE === 'string' ? raw.SECUCODE : '';
208
+ const identity = secucode.includes('.')
209
+ ? normalizeEastmoneySecurityIdentity({ secucode })
210
+ : normalizeEastmoneySecurityIdentity({ secucode: raw.SECUCODE, security_code: raw.SECURITY_CODE, market: raw.MARKET });
211
+ const tradeDate = String(raw.TRADE_DATE ?? '').slice(0, 10);
212
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(tradeDate))
213
+ throw sourceError('Eastmoney billboard row has invalid TRADE_DATE', 'eastmoney_invalid_response');
214
+ return {
215
+ ...identity,
216
+ name: String(raw.SECURITY_NAME_ABBR ?? ''), market: String(raw.MARKET ?? identity.eastmoney_market), trade_date: tradeDate, trade_date_ms: dateMs(tradeDate),
217
+ close_price: numberOrNull(raw.CLOSE_PRICE), change_pct: numberOrNull(raw.CHANGE_RATE), turnover_rate_pct: numberOrNull(raw.TURNOVERRATE),
218
+ billboard_buy_amt: numberOrNull(raw.BILLBOARD_BUY_AMT), billboard_sell_amt: numberOrNull(raw.BILLBOARD_SELL_AMT), billboard_net_amt: numberOrNull(raw.BILLBOARD_NET_AMT), billboard_deal_amt: numberOrNull(raw.BILLBOARD_DEAL_AMT),
219
+ deal_amount_ratio_pct: numberOrNull(raw.DEAL_AMOUNT_RATIO), deal_net_ratio_pct: numberOrNull(raw.DEAL_NET_RATIO), market_deal_amt: numberOrNull(raw.ACCUM_AMOUNT), free_market_cap: numberOrNull(raw.FREE_MARKET_CAP),
220
+ explanation: textOrNull(raw.EXPLANATION), explain: textOrNull(raw.EXPLAIN), trade_market: textOrNull(raw.TRADE_MARKET), trade_id: numberOrNull(raw.TRADE_ID),
221
+ };
222
+ }
223
+ async function executeBillboard(params, signal, ticker) {
224
+ const url = new URL(DATACENTER_URL);
225
+ url.searchParams.set('reportName', 'RPT_DAILYBILLBOARD_DETAILS');
226
+ url.searchParams.set('columns', 'ALL');
227
+ url.searchParams.set('source', 'WEB');
228
+ url.searchParams.set('client', 'WEB');
229
+ url.searchParams.set('sortColumns', 'TRADE_DATE,SECURITY_CODE');
230
+ url.searchParams.set('sortTypes', '-1,1');
231
+ url.searchParams.set('pageNumber', String(params.page));
232
+ url.searchParams.set('pageSize', String(params.size));
233
+ url.searchParams.set('filter', topFilter(params, ticker));
234
+ const payload = await getJson(url.toString(), signal, 'daily billboard');
235
+ if (ticker && payload.success !== true && Number(payload.code) === 9201) {
236
+ const exists = await eastmoneyTickerExists(ticker, signal);
237
+ if (!exists)
238
+ throw sourceError(`Eastmoney ticker ${ticker} is not a recognized security`, 'eastmoney_invalid_ticker');
239
+ throw sourceError(`Eastmoney ticker ${ticker} has no billboard records from ${params.start_date} to ${params.end_date}`, 'eastmoney_no_billboard_data');
240
+ }
241
+ const result = requireDatacenterResult(payload, 'daily billboard');
242
+ const parsed = requireRows(result, 'daily billboard');
243
+ const item = parsed.rows.map(parseTopRow);
244
+ return { data: { item, pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(topBuySellRow) };
245
+ }
246
+ function parseLockupRow(raw) {
247
+ const identity = normalizeEastmoneyAshareIdentity(raw.SECURITY_CODE);
248
+ const freeDate = String(raw.FREE_DATE ?? '').slice(0, 10);
249
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(freeDate))
250
+ throw sourceError('Eastmoney lockup row has invalid FREE_DATE', 'eastmoney_invalid_response');
251
+ return {
252
+ ...identity, name: String(raw.SECURITY_NAME_ABBR ?? ''), free_date: freeDate, free_date_ms: dateMs(freeDate), free_shares_type: textOrNull(raw.FREE_SHARES_TYPE),
253
+ free_shares_raw: numberOrNull(raw.FREE_SHARES), total_ratio: numberOrNull(raw.TOTAL_RATIO), non_free_shares_raw: numberOrNull(raw.NON_FREE_SHARES), able_free_shares_raw: numberOrNull(raw.ABLE_FREE_SHARES),
254
+ };
255
+ }
256
+ async function executeLockup(params, signal) {
257
+ const url = new URL(DATACENTER_URL);
258
+ url.searchParams.set('reportName', 'RPT_LIFT_STAGE');
259
+ url.searchParams.set('columns', 'SECURITY_CODE,SECURITY_NAME_ABBR,FREE_DATE,FREE_SHARES_TYPE,FREE_SHARES,TOTAL_RATIO,NON_FREE_SHARES,ABLE_FREE_SHARES');
260
+ url.searchParams.set('source', 'WEB');
261
+ url.searchParams.set('client', 'WEB');
262
+ url.searchParams.set('sortColumns', 'FREE_DATE');
263
+ url.searchParams.set('sortTypes', '1');
264
+ url.searchParams.set('pageNumber', String(params.page));
265
+ url.searchParams.set('pageSize', String(params.size));
266
+ url.searchParams.set('filter', `(FREE_DATE>='${params.start_date}')(FREE_DATE<='${params.end_date}')`);
267
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'lockup calendar'), 'lockup calendar');
268
+ const parsed = requireRows(result, 'lockup calendar');
269
+ return { data: { item: parsed.rows.map(parseLockupRow), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(lockupRow) };
270
+ }
271
+ function boardParams(params, cashflow) {
272
+ const url = new URL(CLIST_URL);
273
+ url.searchParams.set('pn', String(params.page));
274
+ url.searchParams.set('pz', String(params.size));
275
+ url.searchParams.set('po', '1');
276
+ url.searchParams.set('np', '1');
277
+ url.searchParams.set('ut', EASTMONEY_UT);
278
+ url.searchParams.set('fltt', '2');
279
+ url.searchParams.set('invt', '2');
280
+ url.searchParams.set('fid', cashflow ? 'f62' : SORT_FIELDS[String(params.sort_field)]);
281
+ url.searchParams.set('fs', BOARD_FS[String(params.board_type)]);
282
+ url.searchParams.set('fields', cashflow ? 'f12,f14,f2,f3,f62,f184,f66,f69,f72,f75,f78,f81,f84,f87,f124' : 'f12,f14,f2,f3,f4,f5,f6,f7,f8,f9,f10,f124');
283
+ return url;
284
+ }
285
+ function requireCList(payload, context) {
286
+ if (Number(payload.rc) !== 0 || !isRecord(payload.data))
287
+ throw sourceError(`Eastmoney ${context} failed: ${String(payload.msg ?? 'unknown upstream error')}`, 'eastmoney_upstream_error');
288
+ return payload.data;
289
+ }
290
+ function parseBoardRow(raw, boardType, cashflow) {
291
+ const identity = normalizeEastmoneyBoardIdentity(raw.f12, raw.f14, boardType);
292
+ const row = {
293
+ ...identity, last_price: numberOrNull(raw.f2), change_pct: numberOrNull(raw.f3), change_amount: numberOrNull(raw.f4), volume: numberOrNull(raw.f5), amount: numberOrNull(raw.f6), amplitude_pct: numberOrNull(raw.f7), turnover_pct: numberOrNull(raw.f8), pe: numberOrNull(raw.f9), pb: numberOrNull(raw.f10), eastmoney_server_time: numberOrNull(raw.f124),
294
+ };
295
+ if (cashflow)
296
+ Object.assign(row, {
297
+ main_net_inflow: numberOrNull(raw.f62), main_net_inflow_pct: numberOrNull(raw.f184), super_large_net_inflow: numberOrNull(raw.f66), super_large_net_inflow_pct: numberOrNull(raw.f69), large_net_inflow: numberOrNull(raw.f72), large_net_inflow_pct: numberOrNull(raw.f75), medium_net_inflow: numberOrNull(raw.f78), medium_net_inflow_pct: numberOrNull(raw.f81), small_net_inflow: numberOrNull(raw.f84), small_net_inflow_pct: numberOrNull(raw.f87),
298
+ });
299
+ return row;
300
+ }
301
+ async function executeBoard(params, signal, cashflow) {
302
+ const boardType = String(params.board_type);
303
+ const payload = await getJson(boardParams(params, cashflow).toString(), signal, cashflow ? 'sector cashflow' : 'sector rotation');
304
+ const data = requireCList(payload, cashflow ? 'sector cashflow' : 'sector rotation');
305
+ if (!Array.isArray(data.diff) || typeof data.total !== 'number' || !data.diff.every(isRecord))
306
+ throw sourceError('Eastmoney board result shape changed', 'eastmoney_invalid_response');
307
+ const row = cashflow ? cashflowRow : sectorRow;
308
+ return { data: { item: data.diff.map((raw) => parseBoardRow(raw, boardType, cashflow)), pagination: pagination(Number(params.page), Number(params.size), Math.ceil(data.total / Number(params.size)), data.total) }, schema: { type: 'object', properties: { item: { type: 'array', items: row }, pagination: { type: 'object', additionalProperties: true } }, required: ['item', 'pagination'], additionalProperties: true } };
309
+ }
310
+ function macroDate(value, format, context) {
311
+ if (format === 'chinese') {
312
+ const match = String(value ?? '').match(/(\d{4})年(\d{1,2})月(\d{1,2})日/);
313
+ if (!match)
314
+ throw sourceError(`Eastmoney ${context} row has an unparseable Chinese date`, 'eastmoney_invalid_response');
315
+ return `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`;
316
+ }
317
+ const text = String(value ?? '').slice(0, 10);
318
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(text))
319
+ throw sourceError(`Eastmoney ${context} row has invalid ${context} date`, 'eastmoney_invalid_response');
320
+ return text;
321
+ }
322
+ const macroRow = (table) => {
323
+ const properties = {};
324
+ const nullableNumber = { oneOf: [{ type: 'number' }, { type: 'null' }] };
325
+ for (const date of table.dates) {
326
+ properties[date.to] = { type: 'string' };
327
+ properties[`${date.to}_ms`] = { type: 'integer' };
328
+ }
329
+ if (table.labelFrom)
330
+ properties.period_label = { oneOf: [{ type: 'string' }, { type: 'null' }] };
331
+ for (const column of table.numbers)
332
+ properties[column.to] = nullableNumber;
333
+ for (const column of table.texts ?? [])
334
+ properties[column.to] = { oneOf: [{ type: 'string' }, { type: 'null' }] };
335
+ return { type: 'object', properties, additionalProperties: true };
336
+ };
337
+ function parseMacroRow(table, raw) {
338
+ const row = {};
339
+ for (const date of table.dates) {
340
+ const text = macroDate(raw[date.from], date.format, table.context);
341
+ row[date.to] = text;
342
+ row[`${date.to}_ms`] = dateMs(text);
343
+ }
344
+ if (table.labelFrom)
345
+ row.period_label = textOrNull(raw[table.labelFrom]);
346
+ for (const column of table.numbers)
347
+ row[column.to] = numberOrNull(raw[column.from]);
348
+ for (const column of table.texts ?? [])
349
+ row[column.to] = textOrNull(raw[column.from]);
350
+ return row;
351
+ }
352
+ async function executeMacro(table, params, signal) {
353
+ const url = new URL(DATACENTER_URL);
354
+ url.searchParams.set('reportName', table.reportName);
355
+ url.searchParams.set('columns', 'ALL');
356
+ url.searchParams.set('source', 'WEB');
357
+ url.searchParams.set('client', 'WEB');
358
+ url.searchParams.set('sortColumns', 'REPORT_DATE');
359
+ url.searchParams.set('sortTypes', '-1');
360
+ url.searchParams.set('pageNumber', String(params.page));
361
+ url.searchParams.set('pageSize', String(params.size));
362
+ url.searchParams.set('filter', `(REPORT_DATE>='${params.start_date}')(REPORT_DATE<='${params.end_date}')`);
363
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, table.context), table.context);
364
+ const parsed = requireRows(result, table.context);
365
+ return { data: { item: parsed.rows.map((raw) => parseMacroRow(table, raw)), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(macroRow(table)) };
366
+ }
367
+ function createMacroSource(table) {
368
+ return createSource({
369
+ capability: table.capability,
370
+ name: table.name,
371
+ summary: table.summary,
372
+ description: table.description,
373
+ inputSchema: dateRangeInput,
374
+ outputSchema: datacenterOutput(macroRow(table)),
375
+ paginated: true,
376
+ rowShape: { rowKey: 'item' },
377
+ allowed: ['start_date', 'end_date', 'page', 'size'],
378
+ normalize: (params) => normalizeDateRange(params, ['start_date', 'end_date', 'page', 'size']),
379
+ execute: (params, signal) => executeMacro(table, params, signal),
380
+ });
381
+ }
382
+ const MACRO_PERCENT_NOTE = '`_yoy_pct` 是同比、`_mom_pct` 是环比,都是百分数原值(0.8 表示 +0.8%,不是 0.8%×100 的小数形式);`_ms` 后缀是按 Asia/Shanghai 零点换算的毫秒时间戳。';
383
+ const MACRO_TABLES = [
384
+ {
385
+ capability: 'eastmoney_cpi', name: 'get_eastmoney_cpi', context: 'CPI', reportName: 'RPT_ECONOMY_CPI',
386
+ summary: 'CPI 居民消费价格指数(月度,全国/城市/农村)',
387
+ description: `按月获取中国 CPI(东财 datacenter RPT_ECONOMY_CPI,实测 2010-01 起共 224 期,可深翻到 2010 年)。${MACRO_PERCENT_NOTE} \`_index\` 是当月价格指数(上年同月=100,100.8 即同比 +0.8%),\`_ytd_index\` 是本年 1 月至当月的累计指数(上年同期=100);national / city / rural 三档同口径,实测同期三档同时有值。东财原文月份标签存在 period_label。`,
388
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
389
+ numbers: [
390
+ { from: 'NATIONAL_SAME', to: 'national_yoy_pct' }, { from: 'NATIONAL_BASE', to: 'national_index' }, { from: 'NATIONAL_SEQUENTIAL', to: 'national_mom_pct' }, { from: 'NATIONAL_ACCUMULATE', to: 'national_ytd_index' },
391
+ { from: 'CITY_SAME', to: 'city_yoy_pct' }, { from: 'CITY_BASE', to: 'city_index' }, { from: 'CITY_SEQUENTIAL', to: 'city_mom_pct' }, { from: 'CITY_ACCUMULATE', to: 'city_ytd_index' },
392
+ { from: 'RURAL_SAME', to: 'rural_yoy_pct' }, { from: 'RURAL_BASE', to: 'rural_index' }, { from: 'RURAL_SEQUENTIAL', to: 'rural_mom_pct' }, { from: 'RURAL_ACCUMULATE', to: 'rural_ytd_index' },
393
+ ],
394
+ },
395
+ {
396
+ capability: 'eastmoney_ppi', name: 'get_eastmoney_ppi', context: 'PPI', reportName: 'RPT_ECONOMY_PPI',
397
+ summary: 'PPI 工业生产者出厂价格指数(月度)',
398
+ description: `按月获取中国 PPI(RPT_ECONOMY_PPI,实测 2010-01 起共 248 期)。${MACRO_PERCENT_NOTE} 本表**只有同比**:monthly_index 是当月同比指数(上年同月=100),yoy_pct 是同比百分数,ytd_index 是累计指数;上游不提供环比,不要拿相邻两月自行当成环比。`,
399
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
400
+ numbers: [{ from: 'BASE', to: 'monthly_index' }, { from: 'BASE_SAME', to: 'yoy_pct' }, { from: 'BASE_ACCUMULATE', to: 'ytd_index' }],
401
+ },
402
+ {
403
+ capability: 'eastmoney_gdp', name: 'get_eastmoney_gdp', context: 'GDP', reportName: 'RPT_ECONOMY_GDP',
404
+ summary: 'GDP 国内生产总值与三次产业(季度累计)',
405
+ description: `按季度获取中国 GDP(RPT_ECONOMY_GDP,实测 2006-Q1 起共 82 期)。${MACRO_PERCENT_NOTE} 金额字段是**年初至报告期的累计值**(如 report_date=2026-06-01 那行是上半年累计),单位按东财标称为亿元——未经逐值核对,只用于量级与趋势比较,不要自行换算成美元或绝对口径写进结论;\`_yoy_pct\` 是同比增长率百分数。primary/secondary/tertiary 为三次产业,gdp_value 与 primary 等字段名里的 ytd 后缀是"累计"而非"同比"。`,
406
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
407
+ numbers: [
408
+ { from: 'DOMESTICL_PRODUCT_BASE', to: 'gdp_ytd' }, { from: 'FIRST_PRODUCT_BASE', to: 'primary_ytd' }, { from: 'SECOND_PRODUCT_BASE', to: 'secondary_ytd' }, { from: 'THIRD_PRODUCT_BASE', to: 'tertiary_ytd' },
409
+ { from: 'SUM_SAME', to: 'gdp_yoy_pct' }, { from: 'FIRST_SAME', to: 'primary_yoy_pct' }, { from: 'SECOND_SAME', to: 'secondary_yoy_pct' }, { from: 'THIRD_SAME', to: 'tertiary_yoy_pct' },
410
+ ],
411
+ },
412
+ {
413
+ capability: 'eastmoney_pmi', name: 'get_eastmoney_pmi', context: 'PMI', reportName: 'RPT_ECONOMY_PMI',
414
+ summary: 'PMI 采购经理人指数(制造业与非制造业)',
415
+ description: `按月获取中国 PMI(RPT_ECONOMY_PMI,实测 2010-02 起共 225 期)。manufacturing_pmi / non_manufacturing_pmi 是指数原值(50 为荣枯线,不是百分数);\`_yoy_pct\` 是东财给出的**同比变化百分数**(小数位很多,属东财计算口径),${MACRO_PERCENT_NOTE} 本表不含新订单、生产、就业等分项指数,只有综合两项。`,
416
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
417
+ numbers: [{ from: 'MAKE_INDEX', to: 'manufacturing_pmi' }, { from: 'MAKE_SAME', to: 'manufacturing_yoy_pct' }, { from: 'NMAKE_INDEX', to: 'non_manufacturing_pmi' }, { from: 'NMAKE_SAME', to: 'non_manufacturing_yoy_pct' }],
418
+ },
419
+ {
420
+ capability: 'eastmoney_money_supply', name: 'get_eastmoney_money_supply', context: '货币供应量', reportName: 'RPT_ECONOMY_CURRENCY_SUPPLY',
421
+ summary: 'M0/M1/M2 货币供应量(月度)',
422
+ description: `按月获取人民币 M2 / M1 / M0(RPT_ECONOMY_CURRENCY_SUPPLY,实测 2010-01 起共 224 期)。m2/m1/m0 是**存量原值**,单位按东财标称为亿元(未逐值核对,只做量级与趋势比较);\`_yoy_pct\` 同比、\`_mom_pct\` 环比为百分数,${MACRO_PERCENT_NOTE} 本表没有 M2-M1 剪刀差,需要就自己按两列相减并写明是派生值。`,
423
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
424
+ numbers: [
425
+ { from: 'BASIC_CURRENCY', to: 'm2' }, { from: 'BASIC_CURRENCY_SAME', to: 'm2_yoy_pct' }, { from: 'BASIC_CURRENCY_SEQUENTIAL', to: 'm2_mom_pct' },
426
+ { from: 'CURRENCY', to: 'm1' }, { from: 'CURRENCY_SAME', to: 'm1_yoy_pct' }, { from: 'CURRENCY_SEQUENTIAL', to: 'm1_mom_pct' },
427
+ { from: 'FREE_CASH', to: 'm0' }, { from: 'FREE_CASH_SAME', to: 'm0_yoy_pct' }, { from: 'FREE_CASH_SEQUENTIAL', to: 'm0_mom_pct' },
428
+ ],
429
+ },
430
+ {
431
+ capability: 'eastmoney_rmb_loan', name: 'get_eastmoney_rmb_loan', context: '人民币贷款', reportName: 'RPT_ECONOMY_RMB_LOAN',
432
+ summary: '新增人民币贷款(月度,含负值)',
433
+ description: `按月获取新增人民币贷款(RPT_ECONOMY_RMB_LOAN,实测 2010-01 起共 224 期)。new_loans 是**当月新增额**、ytd_new_loans 是本年累计,单位按东财标称为亿元(未逐值核对);当月新增**可以为负**(贷款净减少,实测 2025-07 为 -5896),负值是事实、不是脏数据,不要过滤掉。${MACRO_PERCENT_NOTE} 本表是人民银行口径的人民币贷款,不含外币贷款与社融增量。`,
434
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
435
+ numbers: [{ from: 'RMB_LOAN', to: 'new_loans' }, { from: 'RMB_LOAN_SAME', to: 'new_loans_yoy_pct' }, { from: 'RMB_LOAN_SEQUENTIAL', to: 'new_loans_mom_pct' }, { from: 'RMB_LOAN_ACCUMULATE', to: 'ytd_new_loans' }, { from: 'LOAN_ACCUMULATE_SAME', to: 'ytd_new_loans_yoy_pct' }],
436
+ },
437
+ {
438
+ capability: 'eastmoney_customs_trade', name: 'get_eastmoney_customs_trade', context: '进出口', reportName: 'RPT_ECONOMY_CUSTOMS',
439
+ summary: '海关进出口金额与同比环比(月度)',
440
+ description: `按月获取出口与进口(RPT_ECONOMY_CUSTOMS,实测 2010-01 起共 224 期)。${MACRO_PERCENT_NOTE} ⛔ **金额字段(export_value / import_value / *_ytd)的计量单位未经核验**:同一列按"千美元"或"万元人民币"解释都能与公开量级对上其一,无法据此定档,所以只能用于趋势与同环比比较,**禁止换算成"元"或"美元"写进结论**;export_yoy_pct / export_mom_pct 等百分数字段口径明确(东财原值)。要绝对额请改走 web_retriever 的海关官方统计材料。本表是海关口径,不含贸易差额字段,需要差额就自己按进出口两列相减并标注派生。`,
441
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
442
+ numbers: [
443
+ { from: 'EXIT_BASE', to: 'export_value' }, { from: 'IMPORT_BASE', to: 'import_value' },
444
+ { from: 'EXIT_BASE_SAME', to: 'export_yoy_pct' }, { from: 'IMPORT_BASE_SAME', to: 'import_yoy_pct' },
445
+ { from: 'EXIT_BASE_SEQUENTIAL', to: 'export_mom_pct' }, { from: 'IMPORT_BASE_SEQUENTIAL', to: 'import_mom_pct' },
446
+ { from: 'EXIT_ACCUMULATE', to: 'export_ytd' }, { from: 'IMPORT_ACCUMULATE', to: 'import_ytd' },
447
+ { from: 'EXIT_ACCUMULATE_SAME', to: 'export_ytd_yoy_pct' }, { from: 'IMPORT_ACCUMULATE_SAME', to: 'import_ytd_yoy_pct' },
448
+ ],
449
+ },
450
+ {
451
+ capability: 'eastmoney_retail_sales', name: 'get_eastmoney_retail_sales', context: '社会消费品零售', reportName: 'RPT_ECONOMY_TOTAL_RETAIL',
452
+ summary: '社会消费品零售总额(月度)',
453
+ description: `按月获取社会消费品零售总额(RPT_ECONOMY_TOTAL_RETAIL,实测 2008-10 起共 209 期)。total_retail 是当月额、ytd_total_retail 是本年累计,单位按东财标称为亿元(未逐值核对,只做量级与趋势比较);${MACRO_PERCENT_NOTE} ⛔ 实测 200 行里有 15 行 total_retail 与 total_retail_yoy_pct 为 **null**、30 行 total_retail_mom_pct 为 null(口径调整与统计口径缺口),null 一律原样保留、不补 0、也不当作 0 增长参与排序。`,
454
+ dates: [{ from: 'REPORT_DATE', to: 'report_date', format: 'datetime' }], labelFrom: 'TIME',
455
+ numbers: [{ from: 'RETAIL_TOTAL', to: 'total_retail' }, { from: 'RETAIL_TOTAL_SAME', to: 'total_retail_yoy_pct' }, { from: 'RETAIL_TOTAL_SEQUENTIAL', to: 'total_retail_mom_pct' }, { from: 'RETAIL_TOTAL_ACCUMULATE', to: 'ytd_total_retail' }, { from: 'RETAIL_ACCUMULATE_SAME', to: 'ytd_total_retail_yoy_pct' }],
456
+ },
457
+ {
458
+ capability: 'eastmoney_deposit_reserve', name: 'get_eastmoney_deposit_reserve', context: '存款准备金率', reportName: 'RPT_ECONOMY_DEPOSIT_RESERVE',
459
+ summary: '存款准备金率调整事件(历次)',
460
+ description: `获取央行**历次存款准备金率调整**(RPT_ECONOMY_DEPOSIT_RESERVE,实测 2007-01 起共 58 条、最新一条 2025-05)。这是**事件表不是月度序列**:一行一次调整,区间内没有调整就是空结果(实测 2025-01..2025-04 返回上游 9201 空,属真实数据缺口,不重试)。report_date 与 announcement/publish_date 口径不同:report_date 与 publish_date 是公告日,effective_date(TRADE_DATE_NEW)才是**生效日**,回答"什么时候开始降"要用 effective_date。large_* / small_* 分别是大型与中小型金融机构存准率(百分数原值,10 表示 10%),large_change_pct / small_change_pct 是本次变动百分点(-0.5 表示下调 0.5 个百分点)。sse_next_change_pct / szse_next_change_pct 是东财附带的**公告次日**上证/深证涨跌幅(百分数),属派生观察值,不要当成市场长期反应。announcement 是央行公告原文,实测 58 条中 30 条为 null。`,
461
+ dates: [
462
+ { from: 'REPORT_DATE', to: 'report_date', format: 'datetime' },
463
+ { from: 'TRADE_DATE_NEW', to: 'effective_date', format: 'datetime' },
464
+ { from: 'PUBLISH_DATE', to: 'publish_date', format: 'chinese' },
465
+ ],
466
+ labelFrom: 'MONTH_DATE',
467
+ numbers: [
468
+ { from: 'INTEREST_RATE_BB', to: 'large_before_pct' }, { from: 'INTEREST_RATE_BA', to: 'large_after_pct' }, { from: 'CHANGE_RATE_B', to: 'large_change_pct' },
469
+ { from: 'INTEREST_RATE_SB', to: 'small_before_pct' }, { from: 'INTEREST_RATE_SA', to: 'small_after_pct' }, { from: 'CHANGE_RATE_S', to: 'small_change_pct' },
470
+ { from: 'NEXT_SH_RATE', to: 'sse_next_change_pct' }, { from: 'NEXT_SZ_RATE', to: 'szse_next_change_pct' },
471
+ ],
472
+ texts: [{ from: 'REMARK', to: 'announcement' }],
473
+ },
474
+ ];
475
+ /**
476
+ * 沪深港通六个渠道(`docs/dev/tool-schema.md` §10.6:上游给的是 `001`…`006` 这种数字码,
477
+ * 模型侧只认语义名)。渠道身份由上游自己标注的 `MUTUAL_TYPE_NAME` 核对,并用同日的
478
+ * `DEAL_AMT` 加法复核过:`001+003=005`(北向合计)、`002+004=006`(南向合计),实测 2026-09-30
479
+ * 两条等式都精确成立。
480
+ */
481
+ const MUTUAL_CHANNELS = {
482
+ sh_stock_connect: '001',
483
+ hk_connect_sh: '002',
484
+ sz_stock_connect: '003',
485
+ hk_connect_sz: '004',
486
+ north_total: '005',
487
+ south_total: '006',
488
+ };
489
+ const MUTUAL_CHANNEL_NAMES = Object.keys(MUTUAL_CHANNELS);
490
+ const mutualFlowRow = {
491
+ type: 'object',
492
+ properties: {
493
+ trade_date: { type: 'string' }, trade_date_ms: { type: 'integer' },
494
+ channel: { enum: MUTUAL_CHANNEL_NAMES }, channel_code: { type: 'string' },
495
+ deal_amt_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
496
+ buy_amt_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
497
+ sell_amt_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
498
+ net_deal_amt_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
499
+ accum_deal_amt_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
500
+ deal_num: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
501
+ hold_market_cap_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
502
+ quota_balance_text: { oneOf: [{ type: 'string' }, { type: 'null' }] },
503
+ index_close_price: { oneOf: [{ type: 'number' }, { type: 'null' }] },
504
+ index_change_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
505
+ lead_thscode: { oneOf: [{ type: 'string' }, { type: 'null' }] },
506
+ lead_stock_name: { oneOf: [{ type: 'string' }, { type: 'null' }] },
507
+ },
508
+ additionalProperties: true,
509
+ };
510
+ function normalizeMutualFlow(params) {
511
+ const normalized = normalizeDateRange(params, ['start_date', 'end_date', 'channel', 'page', 'size']);
512
+ const channel = params.channel === undefined ? 'north_total' : String(params.channel);
513
+ if (!(channel in MUTUAL_CHANNELS))
514
+ throw new Error(`channel must be one of ${MUTUAL_CHANNEL_NAMES.join(', ')}`);
515
+ return { ...normalized, channel };
516
+ }
517
+ function parseMutualFlowRow(raw, channel) {
518
+ const tradeDate = String(raw.TRADE_DATE ?? '').slice(0, 10);
519
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(tradeDate))
520
+ throw sourceError('Eastmoney mutual flow row has invalid TRADE_DATE', 'eastmoney_invalid_response');
521
+ return {
522
+ trade_date: tradeDate,
523
+ trade_date_ms: dateMs(tradeDate),
524
+ channel,
525
+ channel_code: String(raw.MUTUAL_TYPE ?? MUTUAL_CHANNELS[channel]),
526
+ deal_amt_raw: numberOrNull(raw.DEAL_AMT),
527
+ buy_amt_raw: numberOrNull(raw.BUY_AMT),
528
+ sell_amt_raw: numberOrNull(raw.SELL_AMT),
529
+ net_deal_amt_raw: numberOrNull(raw.NET_DEAL_AMT),
530
+ accum_deal_amt_raw: numberOrNull(raw.ACCUM_DEAL_AMT),
531
+ deal_num: numberOrNull(raw.DEAL_NUM),
532
+ hold_market_cap_raw: numberOrNull(raw.HOLD_MARKET_CAP),
533
+ quota_balance_text: textOrNull(raw.QUOTA_BALANCE_TEXT),
534
+ index_close_price: numberOrNull(raw.INDEX_CLOSE_PRICE),
535
+ index_change_pct: numberOrNull(raw.INDEX_CHANGE_RATE),
536
+ lead_thscode: textOrNull(raw.LEAD_STOCKS_CODE),
537
+ lead_stock_name: textOrNull(raw.LEAD_STOCKS_NAME),
538
+ };
539
+ }
540
+ async function executeMutualFlow(params, signal) {
541
+ const url = new URL(DATACENTER_URL);
542
+ url.searchParams.set('reportName', 'RPT_MUTUAL_DEAL_HISTORY');
543
+ url.searchParams.set('columns', 'ALL');
544
+ url.searchParams.set('source', 'WEB');
545
+ url.searchParams.set('client', 'WEB');
546
+ url.searchParams.set('sortColumns', 'TRADE_DATE');
547
+ url.searchParams.set('sortTypes', '-1');
548
+ url.searchParams.set('pageNumber', String(params.page));
549
+ url.searchParams.set('pageSize', String(params.size));
550
+ url.searchParams.set('filter', `(TRADE_DATE>='${params.start_date}')(TRADE_DATE<='${params.end_date}')(MUTUAL_TYPE="${MUTUAL_CHANNELS[String(params.channel)]}")`);
551
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'mutual flow'), 'mutual flow');
552
+ const parsed = requireRows(result, 'mutual flow');
553
+ const channel = String(params.channel);
554
+ return { data: { item: parsed.rows.map((raw) => parseMutualFlowRow(raw, channel)), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(mutualFlowRow) };
555
+ }
556
+ const mutualQuotaRow = {
557
+ type: 'object',
558
+ properties: {
559
+ trade_date: { type: 'string' }, trade_date_ms: { type: 'integer' },
560
+ channel: { enum: MUTUAL_CHANNEL_NAMES }, channel_code: { type: 'string' },
561
+ channel_label: { type: 'string' }, board: { type: 'string' }, direction: { type: 'string' },
562
+ trade_quota_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
563
+ closed_reason: { oneOf: [{ type: 'string' }, { type: 'null' }] },
564
+ index_code: { oneOf: [{ type: 'string' }, { type: 'null' }] },
565
+ index_name: { oneOf: [{ type: 'string' }, { type: 'null' }] },
566
+ board_code: { oneOf: [{ type: 'string' }, { type: 'null' }] },
567
+ },
568
+ additionalProperties: true,
569
+ };
570
+ /** 上游 `MUTUAL_TYPE_NAME` → 我们的渠道名(额度表只给单渠道,没有合计档)。 */
571
+ const MUTUAL_QUOTA_CHANNELS = { 沪股通: 'sh_stock_connect', 深股通: 'sz_stock_connect', '港股通(沪)': 'hk_connect_sh', '港股通(深)': 'hk_connect_sz' };
572
+ const MUTUAL_QUOTA_CODE_CHANNELS = { '001': 'sh_stock_connect', '002': 'hk_connect_sh', '003': 'sz_stock_connect', '004': 'hk_connect_sz' };
573
+ function parseMutualQuotaRow(raw) {
574
+ const tradeDate = String(raw.TRADE_DATE ?? '').slice(0, 10);
575
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(tradeDate))
576
+ throw sourceError('Eastmoney mutual quota row has invalid TRADE_DATE', 'eastmoney_invalid_response');
577
+ const code = String(raw.MUTUAL_TYPE ?? '');
578
+ const channel = MUTUAL_QUOTA_CODE_CHANNELS[code] ?? MUTUAL_QUOTA_CHANNELS[String(raw.MUTUAL_TYPE_NAME ?? '')];
579
+ if (!channel)
580
+ throw sourceError(`Eastmoney mutual quota row has unknown MUTUAL_TYPE ${code || '(blank)'}`, 'eastmoney_invalid_response');
581
+ return {
582
+ trade_date: tradeDate,
583
+ trade_date_ms: dateMs(tradeDate),
584
+ channel,
585
+ channel_code: code,
586
+ channel_label: String(raw.MUTUAL_TYPE_NAME ?? ''),
587
+ board: String(raw.BOARD_TYPE ?? ''),
588
+ direction: raw.FUNDS_DIRECTION === '南向' ? 'south' : raw.FUNDS_DIRECTION === '北向' ? 'north' : String(raw.FUNDS_DIRECTION ?? ''),
589
+ trade_quota_raw: numberOrNull(raw.TRADE_QUOTA),
590
+ closed_reason: textOrNull(raw.CLOSED_REASON),
591
+ index_code: textOrNull(raw.INDEX_CODE),
592
+ index_name: textOrNull(raw.INDEX_NAME),
593
+ board_code: textOrNull(raw.BOARD_CODE),
594
+ };
595
+ }
596
+ async function executeMutualQuota(params, signal) {
597
+ const url = new URL(DATACENTER_URL);
598
+ url.searchParams.set('reportName', 'RPT_MUTUAL_QUOTA');
599
+ url.searchParams.set('columns', 'ALL');
600
+ url.searchParams.set('source', 'WEB');
601
+ url.searchParams.set('client', 'WEB');
602
+ url.searchParams.set('sortColumns', 'MUTUAL_TYPE');
603
+ url.searchParams.set('sortTypes', '1');
604
+ url.searchParams.set('pageNumber', '1');
605
+ url.searchParams.set('pageSize', '10');
606
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'mutual quota'), 'mutual quota');
607
+ const parsed = requireRows(result, 'mutual quota');
608
+ return { data: { item: parsed.rows.map(parseMutualQuotaRow), pagination: pagination(1, 10, parsed.pages, parsed.total) }, schema: datacenterOutput(mutualQuotaRow) };
609
+ }
610
+ /**
611
+ * 个股主力资金快照。上游 `RPT_DMSK_TS_STOCKNEW` 实测**只保留最近一个交易日**的全市场一行一股
612
+ * (count=5199、TRADE_DATE 单一;带旧日期过滤器返回 9201 空),所以这不是时序能力:
613
+ * 契约里必须写死"按日期循环取不到历史",否则模型会拿它当序列反复请求。
614
+ */
615
+ const MAIN_CAPITAL_SORTS = {
616
+ main_net_inflow: 'PRIME_INFLOW',
617
+ change_pct: 'CHANGE_RATE',
618
+ turnover_rate_pct: 'TURNOVERRATE',
619
+ total_score: 'TOTALSCORE',
620
+ };
621
+ const mainCapitalRow = {
622
+ type: 'object',
623
+ properties: {
624
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' },
625
+ trade_date: { type: 'string' }, trade_date_ms: { type: 'integer' },
626
+ close_price: { oneOf: [{ type: 'number' }, { type: 'null' }] },
627
+ change_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
628
+ turnover_rate_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
629
+ pe_dynamic: { oneOf: [{ type: 'number' }, { type: 'null' }] },
630
+ main_net_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] },
631
+ super_large_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] },
632
+ super_large_outflow: { oneOf: [{ type: 'number' }, { type: 'null' }] },
633
+ large_inflow: { oneOf: [{ type: 'number' }, { type: 'null' }] },
634
+ large_outflow: { oneOf: [{ type: 'number' }, { type: 'null' }] },
635
+ main_cost: { oneOf: [{ type: 'number' }, { type: 'null' }] },
636
+ main_cost_20d: { oneOf: [{ type: 'number' }, { type: 'null' }] },
637
+ main_cost_60d: { oneOf: [{ type: 'number' }, { type: 'null' }] },
638
+ super_large_buy_ratio_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
639
+ large_buy_ratio_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
640
+ prime_ratio_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
641
+ prime_ratio_3d_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
642
+ prime_ratio_50d_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
643
+ org_participate_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
644
+ total_score: { oneOf: [{ type: 'number' }, { type: 'null' }] },
645
+ rank: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
646
+ rank_up: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
647
+ focus: { oneOf: [{ type: 'number' }, { type: 'null' }] },
648
+ participate_type: { oneOf: [{ type: 'string' }, { type: 'null' }] },
649
+ },
650
+ additionalProperties: true,
651
+ };
652
+ function normalizeMainCapital(params) {
653
+ assertKnown(params, ['ticker', 'sort_field', 'sort_order', 'page', 'size']);
654
+ const sort_field = params.sort_field === undefined ? 'main_net_inflow' : String(params.sort_field);
655
+ if (!(sort_field in MAIN_CAPITAL_SORTS))
656
+ throw new Error(`sort_field must be one of ${Object.keys(MAIN_CAPITAL_SORTS).join(', ')}`);
657
+ const sort_order = params.sort_order === undefined ? 'desc' : String(params.sort_order);
658
+ if (sort_order !== 'desc' && sort_order !== 'asc')
659
+ throw new Error('sort_order must be desc or asc');
660
+ const page = params.page === undefined ? 1 : integer(params.page, 'page', 1, Number.MAX_SAFE_INTEGER);
661
+ const size = params.size === undefined ? 100 : integer(params.size, 'size', 1, MAX_PAGE_SIZE);
662
+ if (params.ticker === undefined)
663
+ return { sort_field, sort_order, page, size };
664
+ return { sort_field, sort_order, page, size, ticker: normalizeEastmoneySecurityIdentity({ secucode: params.ticker }).thscode };
665
+ }
666
+ function parseMainCapitalRow(raw) {
667
+ const tradeDate = String(raw.TRADE_DATE ?? '').slice(0, 10);
668
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(tradeDate))
669
+ throw sourceError('Eastmoney main capital row has invalid TRADE_DATE', 'eastmoney_invalid_response');
670
+ // SECUCODE 自带市场后缀(实测 200/200 行都有),不做"按代码首位猜市场"的兜底:猜错就是把一只
671
+ // 股票的数据挂到另一只上。缺了就说明上游形状变了,响亮失败。
672
+ if (typeof raw.SECUCODE !== 'string' || !raw.SECUCODE.includes('.')) {
673
+ throw sourceError('Eastmoney main capital row lost its market suffix in SECUCODE', 'eastmoney_invalid_response');
674
+ }
675
+ const identity = normalizeEastmoneySecurityIdentity({ secucode: raw.SECUCODE });
676
+ return {
677
+ ...identity,
678
+ name: String(raw.SECURITY_NAME_ABBR ?? ''),
679
+ trade_date: tradeDate,
680
+ trade_date_ms: dateMs(tradeDate),
681
+ close_price: numberOrNull(raw.CLOSE_PRICE),
682
+ change_pct: numberOrNull(raw.CHANGE_RATE),
683
+ turnover_rate_pct: numberOrNull(raw.TURNOVERRATE),
684
+ pe_dynamic: numberOrNull(raw.PE_DYNAMIC),
685
+ main_net_inflow: numberOrNull(raw.PRIME_INFLOW),
686
+ super_large_inflow: numberOrNull(raw.SUPERDEAL_INFLOW),
687
+ super_large_outflow: numberOrNull(raw.SUPERDEAL_OUTFLOW),
688
+ large_inflow: numberOrNull(raw.BIGDEAL_INFLOW),
689
+ large_outflow: numberOrNull(raw.BIGDEAL_OUTFLOW),
690
+ main_cost: numberOrNull(raw.PRIME_COST),
691
+ main_cost_20d: numberOrNull(raw.PRIME_COST_20DAYS),
692
+ main_cost_60d: numberOrNull(raw.PRIME_COST_60DAYS),
693
+ super_large_buy_ratio_raw: numberOrNull(raw.BUY_SUPERDEAL_RATIO),
694
+ large_buy_ratio_raw: numberOrNull(raw.BUY_BIGDEAL_RATIO),
695
+ prime_ratio_raw: numberOrNull(raw.RATIO),
696
+ prime_ratio_3d_raw: numberOrNull(raw.RATIO_3DAYS),
697
+ prime_ratio_50d_raw: numberOrNull(raw.RATIO_50DAYS),
698
+ org_participate_raw: numberOrNull(raw.ORG_PARTICIPATE),
699
+ total_score: numberOrNull(raw.TOTALSCORE),
700
+ rank: numberOrNull(raw.RANK),
701
+ rank_up: numberOrNull(raw.RANK_UP),
702
+ focus: numberOrNull(raw.FOCUS),
703
+ participate_type: textOrNull(raw.PARTICIPATE_TYPE),
704
+ };
705
+ }
706
+ async function executeMainCapital(params, signal) {
707
+ const url = new URL(DATACENTER_URL);
708
+ url.searchParams.set('reportName', 'RPT_DMSK_TS_STOCKNEW');
709
+ url.searchParams.set('columns', 'ALL');
710
+ url.searchParams.set('source', 'WEB');
711
+ url.searchParams.set('client', 'WEB');
712
+ url.searchParams.set('sortColumns', MAIN_CAPITAL_SORTS[String(params.sort_field)]);
713
+ url.searchParams.set('sortTypes', params.sort_order === 'asc' ? '1' : '-1');
714
+ url.searchParams.set('pageNumber', String(params.page));
715
+ url.searchParams.set('pageSize', String(params.size));
716
+ if (params.ticker)
717
+ url.searchParams.set('filter', `(SECUCODE="${params.ticker}")`);
718
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'main capital snapshot'), 'main capital snapshot');
719
+ const parsed = requireRows(result, 'main capital snapshot');
720
+ return { data: { item: parsed.rows.map(parseMainCapitalRow), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(mainCapitalRow) };
721
+ }
722
+ /**
723
+ * 分红送配方案:一行一个方案,按除权除息日过滤。`PRETAX_BONUS_RMB` / `BONUS_RATIO` / `IT_RATIO`
724
+ * 的"每 10 股"标度由上游自己写在 `IMPL_PLAN_PROFILE` 原文里(实测 `10派280.2423元(含税)` 配
725
+ * `PRETAX_BONUS_RMB=280.2423`、`10送4.00派3.00元` 配 `BONUS_RATIO=4`),所以照原值给出并保留原文。
726
+ * `IS_KCB` 与 `PUBLISH_DATE` 实测 200/200 全为 null,不收录(描述里点名,免得当成取数失败)。
727
+ */
728
+ const dividendRow = {
729
+ type: 'object',
730
+ properties: {
731
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' },
732
+ report_date: { type: 'string' }, plan_notice_date: { type: 'string' }, notice_date: { type: 'string' },
733
+ equity_record_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
734
+ ex_dividend_date: { type: 'string' }, ex_dividend_date_ms: { type: 'integer' },
735
+ ex_dividend_days: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
736
+ assign_progress: { type: 'string' }, plan_profile: { oneOf: [{ type: 'string' }, { type: 'null' }] },
737
+ pretax_cash_per_10: { oneOf: [{ type: 'number' }, { type: 'null' }] },
738
+ bonus_shares_per_10: { oneOf: [{ type: 'number' }, { type: 'null' }] },
739
+ converted_shares_per_10: { oneOf: [{ type: 'number' }, { type: 'null' }] },
740
+ bonus_and_converted_per_10: { oneOf: [{ type: 'number' }, { type: 'null' }] },
741
+ total_shares: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
742
+ basic_eps: { oneOf: [{ type: 'number' }, { type: 'null' }] },
743
+ book_value_per_share: { oneOf: [{ type: 'number' }, { type: 'null' }] },
744
+ capital_reserve_per_share: { oneOf: [{ type: 'number' }, { type: 'null' }] },
745
+ undistributed_profit_per_share: { oneOf: [{ type: 'number' }, { type: 'null' }] },
746
+ net_profit_yoy_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
747
+ dividend_yield_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
748
+ post_10d_change_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
749
+ pre_10d_change_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
750
+ post_30d_change_raw: { oneOf: [{ type: 'number' }, { type: 'null' }] },
751
+ },
752
+ additionalProperties: true,
753
+ };
754
+ /** 日期键归一:`2026-06-26 00:00:00` → `2026-06-26`;空值保留 null(12/200 行没有股权登记日)。 */
755
+ function dayValue(value) {
756
+ const text = String(value ?? '').slice(0, 10);
757
+ return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : null;
758
+ }
759
+ function parseDividendRow(raw) {
760
+ if (typeof raw.SECUCODE !== 'string' || !raw.SECUCODE.includes('.')) {
761
+ throw sourceError('Eastmoney dividend row lost its market suffix in SECUCODE', 'eastmoney_invalid_response');
762
+ }
763
+ const exDate = dayValue(raw.EX_DIVIDEND_DATE);
764
+ if (exDate === null)
765
+ throw sourceError('Eastmoney dividend row has invalid EX_DIVIDEND_DATE', 'eastmoney_invalid_response');
766
+ // 这三列实测 200/200 行都有值(缺了就是上游形状变了),所以按必填处理、缺一就响亮失败;
767
+ // equity_record_date 实测 12/200 为 null(方案还没定登记日),那一列才允许 null。
768
+ const requiredDates = ['REPORT_DATE', 'PLAN_NOTICE_DATE', 'NOTICE_DATE'].map((key) => {
769
+ const day = dayValue(raw[key]);
770
+ if (day === null)
771
+ throw sourceError(`Eastmoney dividend row is missing ${key}`, 'eastmoney_invalid_response');
772
+ return day;
773
+ });
774
+ const [reportDay, planNoticeDay, noticeDay] = requiredDates;
775
+ return {
776
+ ...normalizeEastmoneySecurityIdentity({ secucode: raw.SECUCODE }),
777
+ name: String(raw.SECURITY_NAME_ABBR ?? ''),
778
+ report_date: reportDay,
779
+ plan_notice_date: planNoticeDay,
780
+ notice_date: noticeDay,
781
+ equity_record_date: dayValue(raw.EQUITY_RECORD_DATE),
782
+ ex_dividend_date: exDate,
783
+ ex_dividend_date_ms: dateMs(exDate),
784
+ ex_dividend_days: numberOrNull(raw.EX_DIVIDEND_DAYS),
785
+ assign_progress: String(raw.ASSIGN_PROGRESS ?? ''),
786
+ plan_profile: textOrNull(raw.IMPL_PLAN_PROFILE),
787
+ pretax_cash_per_10: numberOrNull(raw.PRETAX_BONUS_RMB),
788
+ bonus_shares_per_10: numberOrNull(raw.BONUS_RATIO),
789
+ converted_shares_per_10: numberOrNull(raw.IT_RATIO),
790
+ bonus_and_converted_per_10: numberOrNull(raw.BONUS_IT_RATIO),
791
+ total_shares: numberOrNull(raw.TOTAL_SHARES),
792
+ basic_eps: numberOrNull(raw.BASIC_EPS),
793
+ book_value_per_share: numberOrNull(raw.BVPS),
794
+ capital_reserve_per_share: numberOrNull(raw.PER_CAPITAL_RESERVE),
795
+ undistributed_profit_per_share: numberOrNull(raw.PER_UNASSIGN_PROFIT),
796
+ net_profit_yoy_pct: numberOrNull(raw.PNP_YOY_RATIO),
797
+ dividend_yield_raw: numberOrNull(raw.DIVIDENT_RATIO),
798
+ post_10d_change_raw: numberOrNull(raw.D10_CLOSE_ADJCHRATE),
799
+ pre_10d_change_raw: numberOrNull(raw.BD10_CLOSE_ADJCHRATE),
800
+ post_30d_change_raw: numberOrNull(raw.D30_CLOSE_ADJCHRATE),
801
+ };
802
+ }
803
+ function normalizeDividend(params) {
804
+ const normalized = normalizeDateRange(params, ['start_date', 'end_date', 'ticker', 'page', 'size']);
805
+ const identity = normalizeEastmoneySecurityIdentity({ secucode: params.ticker });
806
+ return { ...normalized, ticker: identity.thscode };
807
+ }
808
+ async function executeDividend(params, signal) {
809
+ const url = new URL(DATACENTER_URL);
810
+ url.searchParams.set('reportName', 'RPT_SHAREBONUS_DET');
811
+ url.searchParams.set('columns', 'ALL');
812
+ url.searchParams.set('source', 'WEB');
813
+ url.searchParams.set('client', 'WEB');
814
+ url.searchParams.set('sortColumns', 'EX_DIVIDEND_DATE');
815
+ url.searchParams.set('sortTypes', '-1');
816
+ url.searchParams.set('pageNumber', String(params.page));
817
+ url.searchParams.set('pageSize', String(params.size));
818
+ url.searchParams.set('filter', `(SECUCODE="${params.ticker}")(EX_DIVIDEND_DATE>='${params.start_date}')(EX_DIVIDEND_DATE<='${params.end_date}')`);
819
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'dividend plans'), 'dividend plans');
820
+ const parsed = requireRows(result, 'dividend plans');
821
+ return { data: { item: parsed.rows.map(parseDividendRow), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(dividendRow) };
822
+ }
823
+ /**
824
+ * 股东户数**最新一期截面**(`RPT_HOLDERNUMLATEST`):一股一行,`END_DATE` 是**报告期**、
825
+ * `HOLD_NOTICE_DATE` 才是披露日(实测茅台 END_DATE=2026-06-30 / 披露 2026-08-15),两者必须分开。
826
+ * 新上市标的 `PRE_HOLDER_NUM=0` 且 `HOLDER_NUM_RATIO=null`(无上期可比),不是数据错误。
827
+ */
828
+ const holderRow = {
829
+ type: 'object',
830
+ properties: {
831
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' },
832
+ end_date: { type: 'string' }, end_date_ms: { type: 'integer' },
833
+ hold_notice_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
834
+ pre_end_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
835
+ holder_num: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
836
+ pre_holder_num: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
837
+ holder_num_change: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
838
+ holder_num_ratio_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
839
+ avg_market_cap: { oneOf: [{ type: 'number' }, { type: 'null' }] },
840
+ avg_hold_shares: { oneOf: [{ type: 'number' }, { type: 'null' }] },
841
+ total_market_cap: { oneOf: [{ type: 'number' }, { type: 'null' }] },
842
+ total_a_shares: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
843
+ close_price: { oneOf: [{ type: 'number' }, { type: 'null' }] },
844
+ interval_change_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
845
+ change_shares: { oneOf: [{ type: 'integer' }, { type: 'null' }] },
846
+ change_reason: { oneOf: [{ type: 'string' }, { type: 'null' }] },
847
+ },
848
+ additionalProperties: true,
849
+ };
850
+ function parseHolderRow(raw) {
851
+ if (typeof raw.SECUCODE !== 'string' || !raw.SECUCODE.includes('.')) {
852
+ throw sourceError('Eastmoney holder-count row lost its market suffix in SECUCODE', 'eastmoney_invalid_response');
853
+ }
854
+ const endDay = dayValue(raw.END_DATE);
855
+ if (endDay === null)
856
+ throw sourceError('Eastmoney holder-count row has invalid END_DATE', 'eastmoney_invalid_response');
857
+ return {
858
+ ...normalizeEastmoneySecurityIdentity({ secucode: raw.SECUCODE }),
859
+ name: String(raw.SECURITY_NAME_ABBR ?? ''),
860
+ end_date: endDay,
861
+ end_date_ms: dateMs(endDay),
862
+ hold_notice_date: dayValue(raw.HOLD_NOTICE_DATE),
863
+ pre_end_date: dayValue(raw.PRE_END_DATE),
864
+ holder_num: numberOrNull(raw.HOLDER_NUM),
865
+ pre_holder_num: numberOrNull(raw.PRE_HOLDER_NUM),
866
+ holder_num_change: numberOrNull(raw.HOLDER_NUM_CHANGE),
867
+ holder_num_ratio_pct: numberOrNull(raw.HOLDER_NUM_RATIO),
868
+ avg_market_cap: numberOrNull(raw.AVG_MARKET_CAP),
869
+ avg_hold_shares: numberOrNull(raw.AVG_HOLD_NUM),
870
+ total_market_cap: numberOrNull(raw.TOTAL_MARKET_CAP),
871
+ total_a_shares: numberOrNull(raw.TOTAL_A_SHARES),
872
+ close_price: numberOrNull(raw.CLOSE_PRICE),
873
+ interval_change_pct: numberOrNull(raw.INTERVAL_CHRATE),
874
+ change_shares: numberOrNull(raw.CHANGE_SHARES),
875
+ change_reason: textOrNull(raw.CHANGE_REASON),
876
+ };
877
+ }
878
+ const HOLDER_SORTS = { holder_num: 'HOLDER_NUM', holder_num_ratio_pct: 'HOLDER_NUM_RATIO', total_market_cap: 'TOTAL_MARKET_CAP', end_date: 'END_DATE' };
879
+ function normalizeHolder(params) {
880
+ assertKnown(params, ['ticker', 'sort_field', 'sort_order', 'page', 'size']);
881
+ const sort_field = params.sort_field === undefined ? 'holder_num' : String(params.sort_field);
882
+ if (!(sort_field in HOLDER_SORTS))
883
+ throw new Error(`sort_field must be one of ${Object.keys(HOLDER_SORTS).join(', ')}`);
884
+ const sort_order = params.sort_order === undefined ? 'desc' : String(params.sort_order);
885
+ if (sort_order !== 'desc' && sort_order !== 'asc')
886
+ throw new Error('sort_order must be desc or asc');
887
+ const page = params.page === undefined ? 1 : integer(params.page, 'page', 1, Number.MAX_SAFE_INTEGER);
888
+ const size = params.size === undefined ? 100 : integer(params.size, 'size', 1, MAX_PAGE_SIZE);
889
+ if (params.ticker === undefined)
890
+ return { sort_field, sort_order, page, size };
891
+ return { sort_field, sort_order, page, size, ticker: normalizeEastmoneySecurityIdentity({ secucode: params.ticker }).thscode };
892
+ }
893
+ async function executeHolder(params, signal) {
894
+ const url = new URL(DATACENTER_URL);
895
+ url.searchParams.set('reportName', 'RPT_HOLDERNUMLATEST');
896
+ url.searchParams.set('columns', 'ALL');
897
+ url.searchParams.set('source', 'WEB');
898
+ url.searchParams.set('client', 'WEB');
899
+ url.searchParams.set('sortColumns', HOLDER_SORTS[String(params.sort_field)]);
900
+ url.searchParams.set('sortTypes', params.sort_order === 'asc' ? '1' : '-1');
901
+ url.searchParams.set('pageNumber', String(params.page));
902
+ url.searchParams.set('pageSize', String(params.size));
903
+ if (params.ticker)
904
+ url.searchParams.set('filter', `(SECUCODE="${params.ticker}")`);
905
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'holder count snapshot'), 'holder count snapshot');
906
+ const parsed = requireRows(result, 'holder count snapshot');
907
+ return { data: { item: parsed.rows.map(parseHolderRow), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(holderRow) };
908
+ }
909
+ /**
910
+ * 两融个股明细(`RPTA_WEB_RZRQ_GGMX`)。45 列里绝大多数是同一族的"余额 / 买入 / 偿还 / 净买入 ×
911
+ * 当日 / 3 / 5 / 10 日",所以按映射表成对声明,不逐列写 JSON Schema。
912
+ *
913
+ * 口径靠算术核对,不靠记忆(实测 600519,2026-09-30):`RZRQYE = RZYE + RQYE`、
914
+ * `RZRQYECZ = RZYE - RQYE`、`RZJME = RZMRE - RZCHE`、`RQJMG = RQMCL - RQCHL` 四式精确成立,
915
+ * `RZYEZB = RZYE / SZ`(0.8617% 对得上),所以这几列的含义可以写进契约。
916
+ * 披露新鲜度**跨市场不一致**:实测沪市已到 2026-09-30,深市(000001)最新只有 2026-09-29。
917
+ */
918
+ const MARGIN_COLUMNS = [
919
+ ['RZYE', 'margin_balance'], ['RQYE', 'short_balance'], ['RZRQYE', 'total_balance'], ['RZRQYECZ', 'balance_gap'],
920
+ ['RZMRE', 'margin_buy'], ['RZCHE', 'margin_repay'], ['RZJME', 'margin_net_buy'],
921
+ ['RQMCL', 'short_sell_volume'], ['RQCHL', 'short_repay_volume'], ['RQJMG', 'short_net_buy_volume'],
922
+ ['RZMRE3D', 'margin_buy_3d'], ['RZMRE5D', 'margin_buy_5d'], ['RZMRE10D', 'margin_buy_10d'],
923
+ ['RZCHE3D', 'margin_repay_3d'], ['RZCHE5D', 'margin_repay_5d'], ['RZCHE10D', 'margin_repay_10d'],
924
+ ['RZJME3D', 'margin_net_buy_3d'], ['RZJME5D', 'margin_net_buy_5d'], ['RZJME10D', 'margin_net_buy_10d'],
925
+ ['RQMCL3D', 'short_sell_volume_3d'], ['RQMCL5D', 'short_sell_volume_5d'], ['RQMCL10D', 'short_sell_volume_10d'],
926
+ ['RQCHL3D', 'short_repay_volume_3d'], ['RQCHL5D', 'short_repay_volume_5d'], ['RQCHL10D', 'short_repay_volume_10d'],
927
+ ['RQJMG3D', 'short_net_buy_volume_3d'], ['RQJMG5D', 'short_net_buy_volume_5d'], ['RQJMG10D', 'short_net_buy_volume_10d'],
928
+ ['SZ', 'total_market_cap'], ['SPJ', 'close_price'], ['ZDF', 'change_pct'],
929
+ ['RCHANGE3DCP', 'change_3d_pct'], ['RCHANGE5DCP', 'change_5d_pct'], ['RCHANGE10DCP', 'change_10d_pct'],
930
+ ['RZYEZB', 'margin_balance_pct'], ['FIN_BALANCE_GR', 'margin_balance_growth_raw'],
931
+ ];
932
+ const marginRow = {
933
+ type: 'object',
934
+ properties: {
935
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' },
936
+ trade_date: { type: 'string' }, trade_date_ms: { type: 'integer' },
937
+ market_segment: { oneOf: [{ type: 'string' }, { type: 'null' }] },
938
+ ...Object.fromEntries(MARGIN_COLUMNS.map(([, to]) => [to, { oneOf: [{ type: 'number' }, { type: 'null' }] }])),
939
+ },
940
+ additionalProperties: true,
941
+ };
942
+ function parseMarginRow(raw, requestedThscode) {
943
+ const day = dayValue(raw.DATE);
944
+ if (day === null)
945
+ throw sourceError('Eastmoney margin row has invalid DATE', 'eastmoney_invalid_response');
946
+ // SCODE 不带市场后缀,市场由请求的 ticker 决定:行身份取调用方传入的 canonical 代码,
947
+ // 再与上游 SECUCODE 对照,对不上就是上游把明细串行了。
948
+ const secucode = typeof raw.SECUCODE === 'string' ? raw.SECUCODE : '';
949
+ if (secucode && secucode !== requestedThscode) {
950
+ throw sourceError(`Eastmoney margin row SECUCODE ${secucode} does not match requested ${requestedThscode}`, 'eastmoney_invalid_response');
951
+ }
952
+ const row = {
953
+ thscode: requestedThscode,
954
+ ticker: requestedThscode.replace(/\.(SH|SZ|BJ)$/i, ''),
955
+ name: String(raw.SECNAME ?? ''),
956
+ trade_date: day,
957
+ trade_date_ms: dateMs(day),
958
+ market_segment: textOrNull(raw.MARKET),
959
+ };
960
+ for (const [from, to] of MARGIN_COLUMNS)
961
+ row[to] = numberOrNull(raw[from]);
962
+ return row;
963
+ }
964
+ function normalizeMargin(params) {
965
+ const normalized = normalizeDateRange(params, ['start_date', 'end_date', 'ticker', 'page', 'size']);
966
+ const identity = normalizeEastmoneySecurityIdentity({ secucode: params.ticker });
967
+ return { ...normalized, ticker: identity.thscode };
968
+ }
969
+ async function executeMargin(params, signal) {
970
+ const url = new URL(DATACENTER_URL);
971
+ url.searchParams.set('reportName', 'RPTA_WEB_RZRQ_GGMX');
972
+ url.searchParams.set('columns', 'ALL');
973
+ url.searchParams.set('source', 'WEB');
974
+ url.searchParams.set('client', 'WEB');
975
+ url.searchParams.set('sortColumns', 'DATE');
976
+ url.searchParams.set('sortTypes', '-1');
977
+ url.searchParams.set('pageNumber', String(params.page));
978
+ url.searchParams.set('pageSize', String(params.size));
979
+ url.searchParams.set('filter', `(SCODE="${String(params.ticker).replace(/\.(SH|SZ|BJ)$/i, '')}")(DATE>='${params.start_date}')(DATE<='${params.end_date}')`);
980
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'margin detail'), 'margin detail');
981
+ const parsed = requireRows(result, 'margin detail');
982
+ const thscode = String(params.ticker);
983
+ return { data: { item: parsed.rows.map((raw) => parseMarginRow(raw, thscode)), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(marginRow) };
984
+ }
985
+ /**
986
+ * 可转债发行清单(`RPT_BOND_CB_LIST`,实测全库 1059 只、起息日 2007-07-02 起)。
987
+ *
988
+ * 72 列里只有**条款与日期**有值:`CONVERT_STOCK_PRICE` / `CURRENT_BOND_PRICE`(_NEW) /
989
+ * `TRANSFER_PRICE` / `TRANSFER_PREMIUM_RATIO` / `RESALE_TRIG_PRICE` / `REDEEM_TRIG_PRICE` /
990
+ * `PBV_RATIO` / `MARKET` / `CONVERT_STOCK_PRICEHQ` 实测 **1059/1059 恒为 null**(东财页面上那些
991
+ * 行情数是另一条接口现算的),`TRANSFER_VALUE` 只是 `INITIAL_TRANSFER_PRICE` 的副本(1051/1059 相同
992
+ * 却多 8 个 null)。所以这张表**不承诺行情**,也不把那些 null 当成取数失败。
993
+ */
994
+ const BOND_MARKET_SUFFIX = { CNSESH: 'SH', CNSESZ: 'SZ', STAS00: null };
995
+ function bondStockIdentity(raw) {
996
+ const digits = String(raw.CONVERT_STOCK_CODE ?? '');
997
+ if (!/^\d{6}$/.test(digits))
998
+ throw sourceError(`Eastmoney convertible bond row has invalid CONVERT_STOCK_CODE ${digits || '(empty)'}`, 'eastmoney_invalid_response');
999
+ return {
1000
+ stock_ticker: digits,
1001
+ stock_name: String(raw.SECURITY_SHORT_NAME ?? ''),
1002
+ // 正股市场只取上游 TRADE_MARKET,**不按数字首位推**:实测 15 只 2007-2009 沪市老债代码是 126 开头
1003
+ // (首位 1 也不等于沪),老三板退市债(STAS00)的正股 400266 更不是北交所标的——猜错就是把一只证券
1004
+ // 挂到另一个市场名下。未知市场一律不给后缀(`stock_thscode` 为 null),不猜也不报错。
1005
+ stock_thscode: BOND_MARKET_SUFFIX[String(raw.TRADE_MARKET ?? '')] ? `${digits}.${BOND_MARKET_SUFFIX[String(raw.TRADE_MARKET ?? '')]}` : null,
1006
+ };
1007
+ }
1008
+ const bondRow = {
1009
+ type: 'object',
1010
+ properties: {
1011
+ thscode: { type: 'string' }, ticker: { type: 'string' }, name: { type: 'string' }, market: { type: 'string' },
1012
+ stock_ticker: { type: 'string' }, stock_name: { type: 'string' }, stock_thscode: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1013
+ value_date: { type: 'string' }, value_date_ms: { type: 'integer' },
1014
+ listing_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1015
+ cease_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1016
+ expire_date: { type: 'string' },
1017
+ delist_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1018
+ transfer_start_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1019
+ transfer_end_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1020
+ bond_expire_years: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1021
+ rating: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1022
+ rating_agency: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1023
+ issue_scale_yi: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1024
+ initial_transfer_price: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1025
+ coupon_rate_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1026
+ interest_year_start_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1027
+ interest_year_end_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1028
+ next_cashflow_date: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1029
+ pay_interest_day: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1030
+ interest_rate_explain: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1031
+ first_preplace_per_share_yuan: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1032
+ online_lottery_ratio_pct: { oneOf: [{ type: 'number' }, { type: 'null' }] },
1033
+ in_conversion_period: { oneOf: [{ type: 'string' }, { type: 'null' }] },
1034
+ },
1035
+ additionalProperties: true,
1036
+ };
1037
+ function parseBondRow(raw) {
1038
+ // 债券身份只认上游给的 SECUCODE(带 .SH/.SZ/.NQ 后缀):本表 1059/1059 都有值,丢了后缀就是形状变了。
1039
+ if (typeof raw.SECUCODE !== 'string' || !raw.SECUCODE.includes('.')) {
1040
+ throw sourceError('Eastmoney convertible bond row lost its market suffix in SECUCODE', 'eastmoney_invalid_response');
1041
+ }
1042
+ const valueDate = dayValue(raw.VALUE_DATE);
1043
+ if (valueDate === null)
1044
+ throw sourceError('Eastmoney convertible bond row is missing VALUE_DATE', 'eastmoney_invalid_response');
1045
+ const expireDate = dayValue(raw.EXPIRE_DATE);
1046
+ if (expireDate === null)
1047
+ throw sourceError('Eastmoney convertible bond row is missing EXPIRE_DATE', 'eastmoney_invalid_response');
1048
+ return {
1049
+ thscode: raw.SECUCODE,
1050
+ ticker: String(raw.SECURITY_CODE ?? ''),
1051
+ name: String(raw.SECURITY_NAME_ABBR ?? ''),
1052
+ market: String(raw.TRADE_MARKET ?? ''),
1053
+ ...bondStockIdentity(raw),
1054
+ value_date: valueDate,
1055
+ value_date_ms: dateMs(valueDate),
1056
+ listing_date: dayValue(raw.LISTING_DATE),
1057
+ cease_date: dayValue(raw.CEASE_DATE),
1058
+ expire_date: expireDate,
1059
+ delist_date: dayValue(raw.DELIST_DATE),
1060
+ transfer_start_date: dayValue(raw.TRANSFER_START_DATE),
1061
+ transfer_end_date: dayValue(raw.TRANSFER_END_DATE),
1062
+ bond_expire_years: numberOrNull(raw.BOND_EXPIRE),
1063
+ rating: textOrNull(raw.RATING),
1064
+ rating_agency: textOrNull(raw.PARTY_NAME),
1065
+ issue_scale_yi: numberOrNull(raw.ACTUAL_ISSUE_SCALE),
1066
+ initial_transfer_price: numberOrNull(raw.INITIAL_TRANSFER_PRICE),
1067
+ coupon_rate_pct: numberOrNull(raw.COUPON_IR),
1068
+ interest_year_start_date: dayValue(raw.IB_START_DATE),
1069
+ interest_year_end_date: dayValue(raw.IB_END_DATE),
1070
+ next_cashflow_date: dayValue(raw.CASHFLOW_DATE),
1071
+ pay_interest_day: textOrNull(raw.PAY_INTEREST_DAY),
1072
+ interest_rate_explain: textOrNull(raw.INTEREST_RATE_EXPLAIN),
1073
+ first_preplace_per_share_yuan: numberOrNull(raw.FIRST_PER_PREPLACING),
1074
+ online_lottery_ratio_pct: numberOrNull(raw.ONLINE_GENERAL_LWR),
1075
+ in_conversion_period: textOrNull(raw.IS_CONVERT_STOCK),
1076
+ };
1077
+ }
1078
+ /** 债市代码是 11/12/13/40 等六位数;后缀只用于收窄(实测出现 .SH/.SZ/.NQ),一律剥掉再过滤,不给市场推断留口子。 */
1079
+ function bondCode(value, name) {
1080
+ const raw = String(value).toUpperCase();
1081
+ const digits = raw.replace(/\.[A-Z]{2,3}$/u, '');
1082
+ if (digits !== raw && !/(?:\.SH|\.SZ|\.BJ|\.NQ)$/u.test(raw))
1083
+ throw new Error(`${name} has an unrecognized market suffix: ${raw}`);
1084
+ if (!/^\d{6}$/.test(digits))
1085
+ throw new Error(`${name} must be a six-digit code (a market suffix such as .SH is accepted and ignored)`);
1086
+ return digits;
1087
+ }
1088
+ function normalizeBondList(params) {
1089
+ const normalized = normalizeDateRange(params, ['start_date', 'end_date', 'bond_code', 'stock_code', 'page', 'size']);
1090
+ const result = { ...normalized };
1091
+ if (params.bond_code !== undefined)
1092
+ result.bond_code = bondCode(params.bond_code, 'bond_code');
1093
+ if (params.stock_code !== undefined)
1094
+ result.stock_code = bondCode(params.stock_code, 'stock_code');
1095
+ return result;
1096
+ }
1097
+ async function executeBondList(params, signal) {
1098
+ const url = new URL(DATACENTER_URL);
1099
+ url.searchParams.set('reportName', 'RPT_BOND_CB_LIST');
1100
+ url.searchParams.set('columns', 'ALL');
1101
+ url.searchParams.set('source', 'WEB');
1102
+ url.searchParams.set('client', 'WEB');
1103
+ // sortColumns 与过滤器都用 PUBLIC_START_DATE(实测该列可过滤、可排序,且与 VALUE_DATE 1059/1059 相同);
1104
+ // 只按其一是因为日期区间过滤实测只有这一列**从不为空**——LISTING_DATE 有 8 只未上市的债会被区间静默吞掉。
1105
+ url.searchParams.set('sortColumns', 'PUBLIC_START_DATE');
1106
+ url.searchParams.set('sortTypes', '-1');
1107
+ url.searchParams.set('pageNumber', String(params.page));
1108
+ url.searchParams.set('pageSize', String(params.size));
1109
+ const filters = [`(PUBLIC_START_DATE>='${params.start_date}')(PUBLIC_START_DATE<='${params.end_date}')`];
1110
+ if (params.bond_code !== undefined)
1111
+ filters.push(`(SECURITY_CODE="${params.bond_code}")`);
1112
+ if (params.stock_code !== undefined)
1113
+ filters.push(`(CONVERT_STOCK_CODE="${params.stock_code}")`);
1114
+ url.searchParams.set('filter', filters.join(''));
1115
+ const result = requireDatacenterResult(await getJson(url.toString(), signal, 'convertible bond list'), 'convertible bond list');
1116
+ const parsed = requireRows(result, 'convertible bond list');
1117
+ return { data: { item: parsed.rows.map(parseBondRow), pagination: pagination(Number(params.page), Number(params.size), parsed.pages, parsed.total) }, schema: datacenterOutput(bondRow) };
1118
+ }
1119
+ function createSource(options) {
1120
+ const schema = { capability: options.capability, time_contract: getDataTimeContract(options.capability), name: options.name, source: `http:eastmoney.${options.capability}`, data_key: buildDataKey('eastmoney', 'http', options.capability), source_label: 'eastmoney', paginated: options.paginated === true, cacheMaxAgeMs: options.cacheMaxAgeMs, rowShape: options.rowShape, summary: options.summary, description: options.description, input_schema: options.inputSchema, output_schema: options.outputSchema };
1121
+ const normalize = (params) => options.normalize(params);
1122
+ return { schema, normalizeParams: normalize, validateOutput: (data) => isRecord(data) && Array.isArray(data.item) && isRecord(data.pagination), execute: async (request, signal) => options.execute(normalize(request.params), signal) };
1123
+ }
1124
+ const dateRangeInput = {
1125
+ type: 'object', properties: { start_date: { type: 'string', description: '起始自然日 YYYY-MM-DD' }, end_date: { type: 'string', description: '截止自然日 YYYY-MM-DD' }, page: { type: 'integer', minimum: 1 }, size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE } }, required: ['start_date', 'end_date'], additionalProperties: false,
1126
+ };
1127
+ const boardInput = {
1128
+ type: 'object', properties: { board_type: { type: 'string', enum: [...BOARD_TYPES] }, sort_field: { type: 'string', enum: Object.keys(SORT_FIELDS) }, page: { type: 'integer', minimum: 1 }, size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE } }, required: [], additionalProperties: false,
1129
+ };
1130
+ const cashflowInput = {
1131
+ type: 'object', properties: { board_type: { type: 'string', enum: [...BOARD_TYPES] }, page: { type: 'integer', minimum: 1 }, size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE } }, required: [], additionalProperties: false,
1132
+ };
1133
+ const mutualFlowInput = {
1134
+ type: 'object',
1135
+ properties: {
1136
+ start_date: { type: 'string', description: '起始自然日 YYYY-MM-DD' },
1137
+ end_date: { type: 'string', description: '截止自然日 YYYY-MM-DD' },
1138
+ channel: { type: 'string', enum: MUTUAL_CHANNEL_NAMES, description: '渠道,默认 north_total(北向合计)' },
1139
+ page: { type: 'integer', minimum: 1 },
1140
+ size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
1141
+ },
1142
+ required: ['start_date', 'end_date'],
1143
+ additionalProperties: false,
1144
+ };
1145
+ const snapshotInput = { type: 'object', properties: {}, required: [], additionalProperties: false };
1146
+ const mainCapitalInput = {
1147
+ type: 'object',
1148
+ properties: {
1149
+ ticker: { type: 'string', description: '完整证券代码(带市场后缀,如 600519.SH);省略则取全市场排名' },
1150
+ sort_field: { type: 'string', enum: Object.keys(MAIN_CAPITAL_SORTS), description: '排序列,默认 main_net_inflow' },
1151
+ sort_order: { type: 'string', enum: ['desc', 'asc'] },
1152
+ page: { type: 'integer', minimum: 1 },
1153
+ size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
1154
+ },
1155
+ required: [],
1156
+ additionalProperties: false,
1157
+ };
1158
+ const holderInput = {
1159
+ type: 'object',
1160
+ properties: {
1161
+ ticker: { type: 'string', description: '完整证券代码(带市场后缀,如 600519.SH);省略则取全市场截面' },
1162
+ sort_field: { type: 'string', enum: Object.keys(HOLDER_SORTS), description: '排序列,默认 holder_num' },
1163
+ sort_order: { type: 'string', enum: ['desc', 'asc'] },
1164
+ page: { type: 'integer', minimum: 1 },
1165
+ size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
1166
+ },
1167
+ required: [],
1168
+ additionalProperties: false,
1169
+ };
1170
+ const tickerRangeInput = {
1171
+ type: 'object',
1172
+ properties: {
1173
+ ticker: { type: 'string', description: '完整证券代码(带市场后缀,如 600519.SH)' },
1174
+ start_date: { type: 'string', description: '除权除息日区间起点 YYYY-MM-DD' },
1175
+ end_date: { type: 'string', description: '除权除息日区间终点 YYYY-MM-DD' },
1176
+ page: { type: 'integer', minimum: 1 },
1177
+ size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
1178
+ },
1179
+ required: ['ticker', 'start_date', 'end_date'],
1180
+ additionalProperties: false,
1181
+ };
1182
+ const bondListInput = {
1183
+ type: 'object',
1184
+ properties: {
1185
+ start_date: { type: 'string', description: '起息日区间起点 YYYY-MM-DD' },
1186
+ end_date: { type: 'string', description: '起息日区间终点 YYYY-MM-DD' },
1187
+ bond_code: { type: 'string', description: '可转债代码(六位数字,可带市场后缀);省略则取区间内全部转债' },
1188
+ stock_code: { type: 'string', description: '正股代码(六位数字,可带市场后缀);与 bond_code 同时给则取交集' },
1189
+ page: { type: 'integer', minimum: 1 },
1190
+ size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
1191
+ },
1192
+ required: ['start_date', 'end_date'],
1193
+ additionalProperties: false,
1194
+ };
1195
+ export function createEastmoneySources() {
1196
+ return [
1197
+ createSource({ capability: 'eastmoney_top_buy_sell_market', name: 'get_eastmoney_top_buy_sell_market', summary: '东财全市场龙虎榜汇总', description: '按交易日区间分页获取东方财富龙虎榜上榜股票汇总。金额字段单位为元,CHANGE_RATE/TURNOVERRATE/DEAL_*_RATIO 为东财百分数原值;同一股票同日可能因多个上榜原因返回多行,不去重。上游 HTTP 200 但 success=false、code 非 0 或 result 缺失均视为失败,不转换为空数组。', inputSchema: dateRangeInput, outputSchema: datacenterOutput(topBuySellRow), paginated: true, rowShape: { rowKey: 'item' }, allowed: ['start_date', 'end_date', 'page', 'size'], normalize: (params) => normalizeDateRange(params, ['start_date', 'end_date', 'page', 'size']), execute: (params, signal) => executeBillboard(params, signal) }),
1198
+ createSource({ capability: 'eastmoney_top_buy_sell_ticker', name: 'get_eastmoney_top_buy_sell_ticker', summary: '东财单票龙虎榜汇总', description: '按单只 A 股和交易日区间获取东方财富龙虎榜汇总。ticker 必须带 SH/SZ/BJ 市场后缀;请求内部转换为 SECURITY_CODE,单票过滤器要求日期使用单引号、代码使用双引号。返回保留同日多条上榜原因;金额单位为元,百分比为东财原值。该能力是上榜股票汇总,不是营业部席位明细。', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '完整证券代码,如 600519.SH' }, start_date: { type: 'string' }, end_date: { type: 'string' }, page: { type: 'integer', minimum: 1 }, size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE } }, required: ['ticker', 'start_date', 'end_date'], additionalProperties: false }, outputSchema: datacenterOutput(topBuySellRow), paginated: true, rowShape: { rowKey: 'item' }, allowed: ['ticker', 'start_date', 'end_date', 'page', 'size'], normalize: normalizeTicker, execute: (params, signal) => executeBillboard(params, signal, String(params.ticker)) }),
1199
+ createSource({ capability: 'eastmoney_lockup_expiry', name: 'get_eastmoney_lockup_expiry', summary: '东财限售解禁日历', description: '按自然日期区间分页获取东方财富限售解禁日历。已验证字段包括解禁日期、解禁股份类型、FREE_SHARES、TOTAL_RATIO、NON_FREE_SHARES、ABLE_FREE_SHARES;TOTAL_RATIO 是小数比例,股份数量字段以 free_shares_raw 等原始数值保存,单位以东财页面口径为准,未擅自标成股。', inputSchema: dateRangeInput, outputSchema: datacenterOutput(lockupRow), paginated: true, rowShape: { rowKey: 'item' }, allowed: ['start_date', 'end_date', 'page', 'size'], normalize: (params) => normalizeDateRange(params, ['start_date', 'end_date', 'page', 'size']), execute: executeLockup }),
1200
+ createSource({ capability: 'eastmoney_sector_rotation', cacheMaxAgeMs: 60_000, name: 'get_eastmoney_sector_rotation', summary: '东财板块行情排名快照', description: '获取东方财富行业、概念或地域板块的查询时点排名快照。默认行业、按涨跌幅排序;board_type 映射为东财 m:90+t:2/3/1。不是历史轮动序列,Dataset 的 captured_at 才是采集时间;板块代码使用 board_code,不归一为证券 ticker。', inputSchema: boardInput, outputSchema: { type: 'object', properties: { item: { type: 'array', items: sectorRow }, pagination: { type: 'object', additionalProperties: true } }, required: ['item', 'pagination'], additionalProperties: true }, paginated: true, rowShape: { rowKey: 'item' }, allowed: ['board_type', 'sort_field', 'page', 'size'], normalize: normalizeBoard, execute: (params, signal) => executeBoard(params, signal, false) }),
1201
+ createSource({ capability: 'eastmoney_cashflow_rotation', cacheMaxAgeMs: 60_000, name: 'get_eastmoney_cashflow_rotation', summary: '东财板块资金流快照', description: '获取东方财富行业、概念或地域板块当前资金流快照,默认按主力净流入排序。f62/f66/f72/f78/f84 为金额原值(元),f184/f69/f75/f81/f87 为东财原始占比;当前只承诺查询时点快照,不把未确认的 5 日/10 日字段映射为历史序列。', inputSchema: cashflowInput, outputSchema: { type: 'object', properties: { item: { type: 'array', items: cashflowRow }, pagination: { type: 'object', additionalProperties: true } }, required: ['item', 'pagination'], additionalProperties: true }, paginated: true, rowShape: { rowKey: 'item' }, allowed: ['board_type', 'page', 'size'], normalize: normalizeCashflowBoard, execute: (params, signal) => executeBoard(params, signal, true) }),
1202
+ ...MACRO_TABLES.map(createMacroSource),
1203
+ createSource({
1204
+ capability: 'eastmoney_mutual_flow', name: 'get_eastmoney_mutual_flow',
1205
+ summary: '沪深港通成交额与额度状态(按渠道日频)',
1206
+ description: '按交易日区间与渠道获取沪深港通成交数据(RPT_MUTUAL_DEAL_HISTORY,实测单渠道日频、可翻到多年前)。channel 六档:北向 sh_stock_connect(沪股通)/ sz_stock_connect(深股通)/ north_total(北向合计,默认),南向 hk_connect_sh / hk_connect_sz / south_total;渠道身份由上游 MUTUAL_TYPE_NAME 自标,实测同一交易日 001+003=005、002+004=006 的成交额加法精确成立。⛔ **北向三档的 buy_amt_raw / sell_amt_raw / net_deal_amt_raw / accum_deal_amt_raw 上游一律为 null**(交易所自 2024-08 起停止披露北向每日买卖明细,实测 2026-09 仍为 null),本能力**不提供"北向净买入"**,谁问就只能给成交额;成交额 deal_amt_raw、成交笔数 deal_num、额度状态 quota_balance_text 与当日领涨股仍披露,南向四档金额字段都有值。金额一律是东财原值(`_raw` 后缀)且**跨渠道计量口径不一致**(配套的额度表里北向 52000 对应官方 520 亿元、南向 42000000000 对应 420 亿元,即同一列两种单位),所以只能在同一渠道内做趋势与相对比较,禁止跨渠道相加、禁止换算成亿元或元写进结论。hold_market_cap_raw 在北向单渠道为 null、北向合计为 0(上游占位,不是"市值为零")。index_change_pct 是东财附带的相关指数涨跌幅(百分数原值),lead_thscode 带市场后缀。',
1207
+ inputSchema: mutualFlowInput, outputSchema: datacenterOutput(mutualFlowRow), paginated: true, rowShape: { rowKey: 'item' },
1208
+ allowed: ['start_date', 'end_date', 'channel', 'page', 'size'], normalize: normalizeMutualFlow, execute: executeMutualFlow,
1209
+ }),
1210
+ createSource({
1211
+ capability: 'eastmoney_main_capital_snapshot', cacheMaxAgeMs: 60_000, name: 'get_eastmoney_main_capital_snapshot',
1212
+ summary: '东财个股主力资金快照(仅最近交易日)',
1213
+ description: '获取东方财富个股主力资金**查询时点快照**(RPT_DMSK_TS_STOCKNEW)。⛔ 上游只保留**最近一个交易日**的全市场一行一股(实测 5199 只、TRADE_DATE 只有当天,带旧日期的过滤器返回 9201 空),本能力**没有历史序列**:不要按日期循环请求,需要历史就改走行情类能力或如实告知用户取不到。默认按主力净流入降序分页;ticker 必须带市场后缀(如 600519.SH),省略则取全市场排名。main_net_inflow 与各档 *_inflow / *_outflow 是东财原值(实测 000501 主力净流入 -3822595,量级对应元);main_cost / main_cost_20d / main_cost_60d 是主力成本价(元/股,用来判断现价高于还是低于主力成本);change_pct 与 turnover_rate_pct 是东财百分数原值(0.6011 表示 0.6011%),与本仓其他东财能力同口径。带 `_raw` 后缀的比例列(*_ratio_raw、org_participate_raw)**口径未经核验**:0.1198 既可能读作 0.12% 也可能读作 11.98%,只能做同列相对比较,禁止换算成百分数写进结论。rank / rank_up / total_score / focus 是东财自有的主力资金排名与关注度打分,口径归东财、跨日不可比;participate_type 是上游的参与类型码,原文保留未做翻译。实测 200 行里 CHANGE_RATE 与 TURNOVERRATE 各有 2 行为 null(停牌或当日无成交),null 原样保留不补 0。Dataset 的 captured_at 才是采集时间。',
1214
+ inputSchema: mainCapitalInput, outputSchema: datacenterOutput(mainCapitalRow), paginated: true, rowShape: { rowKey: 'item' },
1215
+ allowed: ['ticker', 'sort_field', 'sort_order', 'page', 'size'], normalize: normalizeMainCapital, execute: executeMainCapital,
1216
+ }),
1217
+ createSource({
1218
+ capability: 'eastmoney_dividend_plan', name: 'get_eastmoney_dividend_plan',
1219
+ summary: '东财分红送配方案明细(按个股与除权日)',
1220
+ description: '按单只 A 股与除权除息日区间获取东方财富分红送配方案(RPT_SHAREBONUS_DET,实测全库 56976 条、可翻回 1991 年)。一行一个方案;同一报告期可能同时有年度与中期两条方案,用 report_date 加 ex_dividend_date 区分。四个日期口径不同:report_date 是**报告期**(如 2025-12-31),plan_notice_date 是预案公告日,notice_date 是实施公告日,equity_record_date 是股权登记日(实测 12/200 行为 null——方案尚未定登记日,属正常),ex_dividend_date 是除权除息日;只有 ex_dividend_date_ms 提供毫秒时间轴。送转与派息一律是**每 10 股**口径(上游原文 plan_profile 就写成「10派280.2423元(含税)」,照抄即可核对):pretax_cash_per_10 为每 10 股税前派息(元),bonus_shares_per_10 为每 10 股送股,converted_shares_per_10 为每 10 股转增,bonus_and_converted_per_10 为送转合计;纯派息方案的三个送转列同时为 null,不是缺数。assign_progress 是方案进度(实测「实施分配」/「董事会决议通过」);ex_dividend_days 是距除权日天数,**未来为负**(实测 -17)。basic_eps / book_value_per_share / capital_reserve_per_share / undistributed_profit_per_share / net_profit_yoy_pct 是东财随附的每股与利润表指标。带 `_raw` 的三列除权前后涨跌幅与 dividend_yield_raw 的复权与比例口径**未经核验**,只做同列相对比较,禁止换算成百分数写进结论。上游的 IS_KCB 与 PUBLISH_DATE 实测全为 null,本能力不收录,不要当成取数失败;SECURITY_INNER_CODE / ORG_CODE / MARKET_TYPE 等内部编码同样不进取。',
1221
+ inputSchema: tickerRangeInput, outputSchema: datacenterOutput(dividendRow), paginated: true, rowShape: { rowKey: 'item' },
1222
+ allowed: ['ticker', 'start_date', 'end_date', 'page', 'size'], normalize: normalizeDividend, execute: executeDividend,
1223
+ }),
1224
+ createSource({
1225
+ capability: 'eastmoney_holder_number_snapshot', cacheMaxAgeMs: 60_000, name: 'get_eastmoney_holder_number_snapshot',
1226
+ summary: '东财股东户数最新一期截面',
1227
+ description: '获取东方财富股东户数的**最新一期截面**(RPT_HOLDERNUMLATEST,实测 5568 只、一股一行)。这是截面不是序列:上游只给每股当前最新一期,取不到历史区间,需要历史就如实告知用户现有能力覆盖不了。end_date 是**报告期**(如 2026-06-30),hold_notice_date 才是**披露日**(实测茅台报告期 2026-06-30、披露 2026-08-15),pre_end_date 是上一期报告期(新股为 null);时间轴用 end_date_ms,不要把披露日当报告期用。holder_num 是股东户数,pre_holder_num 上期户数,holder_num_change 变动户数,holder_num_ratio_pct 变动比例(百分数原值,21.89 表示 +21.89%)——新股实测 pre_holder_num=0 且 holder_num_ratio_pct=null,这是"无上期可比",不是取数失败。⛔ 基数极小时比值失真到没有分析价值:实测按 holder_num_ratio_pct 降序前五名上期户数是 60/4/6/7/17(长鑫科技 60 → 3,456,664 户给出 +5,761,006.67%,change_reason 多为「发行融资」即新股上市),比值是东财原值、我们不裁剪,按该列排序或求均值前先剔除这类行。end_date 也只是该股**最新披露的一期**,停更票会停在多年前(实测 601865 停在 2019-02-15),别把"最新一期"读成"最近"。avg_market_cap 户均市值、avg_hold_shares 户均持股数、total_market_cap 总市值、total_a_shares A 股总股本、close_price 收盘价均为东财原值(市值与价格为元级量纲,未逐值核对);interval_change_pct 是东财给出的区间涨跌幅(百分数原值);change_shares 与 change_reason 是股本变动数与**中文原因原文**(实测「发行融资」/「资产重组」),原文照存不改写。默认按股东户数降序,可按 holder_num_ratio_pct / total_market_cap / end_date 换排序;ticker 带市场后缀时只返回该只一行。',
1228
+ inputSchema: holderInput, outputSchema: datacenterOutput(holderRow), paginated: true, rowShape: { rowKey: 'item' },
1229
+ allowed: ['ticker', 'sort_field', 'sort_order', 'page', 'size'], normalize: normalizeHolder, execute: executeHolder,
1230
+ }),
1231
+ createSource({
1232
+ capability: 'eastmoney_mutual_quota', cacheMaxAgeMs: 60_000, name: 'get_eastmoney_mutual_quota',
1233
+ summary: '北上南下当日额度与休市状态快照',
1234
+ description: '获取四个互连渠道(沪股通 / 深股通 / 港股通(沪) / 港股通(深))在查询时点当日的额度与开关状态(RPT_MUTUAL_QUOTA,实测全表只有当日四条、没有历史序列)。无参数;Dataset 的 captured_at 才是采集时间,trade_date 是上游给出的日历日。closed_reason 非空表示当日休市(实测 2026-10-05 为「国庆节」);上游的交易时段字段 start_time / end_time 在实测全部为 null,本能力不收录,不要当成缺口。⛔ trade_quota_raw 的计量口径**跨方向不一致**:北向 52000 对应官方每日额度 520 亿元、南向 42000000000 对应 420 亿元(同一列两种单位),因此两个方向的原值不可直接比较,也不要换算后写进结论;要绝对额度请以交易所官方披露为准。channel 是语义名、channel_label 是上游原文,board 为沪港通/深港通、direction 为 north/south。',
1235
+ inputSchema: snapshotInput, outputSchema: datacenterOutput(mutualQuotaRow), paginated: false, rowShape: { rowKey: 'item' },
1236
+ allowed: [], normalize: (params) => { assertKnown(params, []); return {}; }, execute: executeMutualQuota,
1237
+ }),
1238
+ createSource({
1239
+ capability: 'eastmoney_margin_trading', name: 'get_eastmoney_margin_trading',
1240
+ summary: '个股融资融券明细(按票与交易日)',
1241
+ description: '按单只 A 股与交易日区间获取融资融券明细(RPTA_WEB_RZRQ_GGMX,实测 600519 共 3992 个交易日、可翻回约 2010 年)。⛔ **披露新鲜度跨市场不一致**:实测同一天查询沪市(600519)最新到 2026-09-30、深市(000001)最新只有 2026-09-29,深市滞后一个交易日;把两市当天放一起比会得出错误结论,需要同日对齐就自己截到共同日期并写明。金额列单位是元(实测 RZYE=22131853460 即 221 亿元)。余额族:margin_balance 融资余额、short_balance 融券余额、total_balance 两融合计、balance_gap 融资减融券——后两列已用真报文做加减法核对(total=marg…+short、gap=融资-融券,精确成立)。流量族:margin_buy 融资买入额、margin_repay 融资偿还额、margin_net_buy 融资净买入(=买入减偿还,实测精确成立),带 _3d / _5d / _10d 后缀的是**滚动累计而不是日均**,别除以天数。融券流量(short_sell_volume / short_repay_volume / short_net_buy_volume 及其累计)量纲是**股数**,与金额族不可相加。参考列:total_market_cap 总市值(元)、close_price 收盘价(元)、change_pct 当日涨跌幅、change_3d_pct / change_5d_pct / change_10d_pct 区间涨跌幅,均为东财百分数原值;margin_balance_pct 是融资余额占总市值的百分数原值(实测 0.8617 与 RZYE/SZ 一致)。margin_balance_growth_raw 的增长口径未核验,只做同列相对比较。market_segment 是上游市场原文(融资融券_沪证 / 融资融券_深证)。上游的 KCB 标志、TRADE_MARKET(_CODE) 内部编码与不带后缀的 SCODE 不进取;行身份采用请求传入的带市场后缀代码,与上游 SECUCODE 对不上即判 eastmoney_invalid_response。',
1242
+ inputSchema: tickerRangeInput, outputSchema: datacenterOutput(marginRow), paginated: true, rowShape: { rowKey: 'item' },
1243
+ allowed: ['ticker', 'start_date', 'end_date', 'page', 'size'], normalize: normalizeMargin, execute: executeMargin,
1244
+ }),
1245
+ createSource({
1246
+ capability: 'eastmoney_convertible_bond_list', name: 'get_eastmoney_convertible_bond_list',
1247
+ summary: '东财可转债发行清单与条款要素(按起息日)',
1248
+ description: '按起息日区间分页获取东方财富可转债发行清单(RPT_BOND_CB_LIST,实测全库 1059 只、起息日 2007-07-02 至今,含已摘牌债与老三板退市债)。⛔ **本表不含行情**:转债现价、正股现价、最新转股价、回售/赎回触发价、PBV 一类列实测 1059/1059 恒为 null,转股溢价率恒为占位值 100——东财页面上那些数是另一条行情接口现算的;要现价或溢价率走别的来源,不要把那些 null 当成取数失败。区间过滤的是**起息日**(实测与网上申购日 1059/1059 相同,所以只留 value_date 一列),不是上市日:实测 8 只「已发行未上市」的 listing_date 为 null,按上市日过滤会把它们静默吞掉。行主体是**债券**——thscode / ticker / name 是转债本身(如 110059.SH),正股在 stock_ticker / stock_name / stock_thscode,两者不要混用。⛔ 正股市场只取上游 TRADE_MARKET(CNSESH=沪、CNSESZ=深),**不许按代码首位推**:实测 15 只 2007-2009 沪市老债代码是 126 开头;老三板退市债(TRADE_MARKET=STAS00,如 404005.NQ,正股 400266)与任何未知市场都不给 stock_thscode(该列为 null)。量纲:issue_scale_yi 是发行规模**亿元**(实测浦发转债 500、南药转债 10.81491 精确到元);面值与发行价恒为 100 元/张,故不收录;initial_transfer_price 与 first_preplace_per_share_yuan(原股东**每股**可获配的债券面值,元/股)单位是元;coupon_rate_pct 与 online_lottery_ratio_pct(网上发行中签率)是**百分数原值**,0.2 就读作 0.2%。⏱ 随查询日移动的一组:coupon_rate_pct 是**当前计息年度**的票面利率(实测所有非空行的计息区间都跨过查询日,且与阶梯原文逐年对得上——起息 2023-07-18 的福蓉转债给 1.5,正对应「第四年1.50%」),interest_year_start_date / interest_year_end_date / next_cashflow_date 是这个计息年度的起止与付息日;已到期或退市的债这四处实测 467/1059 为 null。完整利率阶梯读 interest_rate_explain——它是上游原文,可能是逐年阶梯,也可能是发行前尚未定案的「预设区间」(实测 126 段老债就是这种),别当成交利率用。bond_expire_years 是**实际存续年数**(等于 expire_date 减 value_date;提前赎回的债按实际终止日算——豫光转债合同 6 年、实测 1.3534),不是合同期限。日期列分职:cease_date 停止交易日、expire_date 到期或终止日、delist_date 摘牌日(null=尚未摘牌;实测 4 条是**将来**日期,即已公告待摘牌)、listing_date 上市日、transfer_start_date / transfer_end_date 转股期起止;cease_date 与 transfer_end_date 实测 180/1059 不一致,故两列都保留。实测 15 只 2007-2009 老债(14 只 126 段沪债加 115003.SZ)没有转股期:transfer_start_date / transfer_end_date / initial_transfer_price / in_conversion_period **同批**为 null,那是产品形态(分离交易可转债)或上游未披露,不是取数失败。in_conversion_period 有值时是上游原文「是/否」,含义是**当前是否处于转股期**(未开始或已结束都给「否」,实测 是:270 / 否:774)。rating 是东财列示的评级原文(实测只有 AAA/AA+/AA/AA-/A+/A 六档,未标明主体还是债项、未标明评级时点,不要当风险结论用),rating_agency 是评级机构名。区间内没有发行就是空结果(上游 code 9201),不要重试。未收录:回售/赎回条款正文(每行数百字符,读条款去看募集说明书)、网上申购上限、ISSUE_TYPE / REDEEM_TYPE / PARAM_NAME / BOND_COMBINE_CODE 等内部编码、FIRST_PROFIT(实测 -898~26946,口径推不出)、TRANSFER_VALUE(与 initial_transfer_price 实测相同却多 8 个 null)。',
1249
+ inputSchema: bondListInput, outputSchema: datacenterOutput(bondRow), paginated: true, rowShape: { rowKey: 'item' }, cacheMaxAgeMs: 3_600_000,
1250
+ allowed: ['start_date', 'end_date', 'bond_code', 'stock_code', 'page', 'size'], normalize: normalizeBondList, execute: executeBondList,
1251
+ }),
1252
+ ];
1253
+ }