@v587d/capital-generation 2.8.3 → 2.9.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.
- package/CHANGELOG.md +81 -1
- package/README.md +40 -19
- package/chart-ui/README.md +5 -4
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +13 -8
- package/lib/chart/artifact-ref.js +27 -10
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +43 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/present-tool.d.ts +28 -0
- package/lib/chart/present-tool.js +128 -0
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +7 -29
- package/lib/chart/tool.js +15 -25
- package/lib/chart-render-entry.d.ts +10 -0
- package/lib/chart-render-entry.js +14 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +41 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/present-entry.d.ts +9 -0
- package/lib/present-entry.js +13 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -34
- package/lib/sources/ftshare-rest.js +84 -292
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +30 -66
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +72 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +532 -337
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -4
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -2
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +30 -33
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +14 -16
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
|
@@ -1,85 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FTShare
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - 同花顺 `capital-flow/*` 对我们是 `code=2004` 永久关闭,东财 `eastmoney_main_capital_snapshot`
|
|
7
|
-
* 实测**只有查询日一股一行**——个股资金流的历史序列在本仓一直是空的。这一族给到最近约 45 个交易日
|
|
8
|
-
* (T-45 仍有 5199 行、T-50 起 total=0),主力五档更给到约 187 个交易日。
|
|
9
|
-
* - 板块(行业)资金滚动此前只有 `eastmoney_cashflow_rotation` 一条,上游一抖就没有第二家。
|
|
10
|
-
* - 北交所行情此前是**零**:腾讯那一路在参数层明确拒 BJ,而这一族实测含 349 只北证票。
|
|
11
|
-
* - 期货与贵金属此前在本仓是**整块空白**(`ticker_search` 只允许消歧到期货交易所,没有一条数据端点),
|
|
12
|
-
* 所以批次 F 那十颗不是第二家,而是第一家:合约清单与要素、日/周/月/季 K 线、涨跌停价、每日结算参数、
|
|
13
|
-
* 仓单日报、品种周报、南华商品指数、东财会员持仓排名、贵金属价格。
|
|
14
|
-
* 上游分别是同花顺与同花顺 daec,与现有能力**不是同一条抓取路径**,才够称 plan B。
|
|
15
|
-
*
|
|
16
|
-
* ⛔ 本模块的 description 与注释**只允许写有原始报文可回查的数字**。2026-10-15 曾有一批"实测计数"
|
|
17
|
-
* (行数在 3766/3864/5450/5574 之间跳、`ys` 返回 309 行、业务码 100605、页码回显谎报)事后逐条回查
|
|
18
|
-
* **在任何一次真报文里都不存在**,是我把不同接口的读数串成了因果;据此撤掉的两颗能力已于当晚复验注册回来。
|
|
19
|
-
* 教训写进 `docs/design/ftshare-source-survey.md` §7.5–§7.7:**没打进日志的数字不算实测**。
|
|
20
|
-
*
|
|
21
|
-
* ⛔ 免费档限流 **10 QPS / burst 20**(官方口径)。`DataCollectorHub` 的 FIFO 只保证同一时刻
|
|
22
|
-
* 一个请求,**不含最小间隔**——串行 ≠ 节流(`src/net/eastmoney-client.ts` 同一条教训)。
|
|
23
|
-
* 本模块用进程级节流链把上限压到 ~8 QPS,留突发余量。
|
|
24
|
-
*
|
|
25
|
-
* ⛔ 凭据一律**调用期解析**,注册只看结构(AGENTS.md §9.7,2026-09-30 桌面端 61 能力静默缺席
|
|
26
|
-
* 那一族事故)。套餐外路由返回 403 `route_not_allowed`,归类成 `plan_closed` 且不重试——
|
|
27
|
-
* 它和 Fuyao 的 `2004` 是同一件事:能力级不可用,重试只白烧回合。
|
|
2
|
+
* FTShare (非凸科技 `market.ft.tech/gateway`): capital flows, quote/K-line, calendars, block trades,
|
|
3
|
+
* SW industry, futures/metals; the only source for A-share flow history, BJ quotes and futures.
|
|
4
|
+
* ⛔ Only numbers verifiable in a logged raw response; free tier 10 QPS/burst 20 (we cap ~8 QPS).
|
|
5
|
+
* ⛔ Credentials resolve per call; 403 route_not_allowed = plan_closed, never retry (cf. Fuyao 2004).
|
|
28
6
|
*/
|
|
29
7
|
import { buildDataKey } from '../data-collector/hub.js';
|
|
30
8
|
import { getDataTimeContract } from '../time/tools.js';
|
|
31
9
|
import { createRequestThrottle } from '../net/throttle.js';
|
|
32
10
|
import { parseSecurityCode } from './security-code.js';
|
|
33
11
|
const FTSHARE_BASE_URL = 'https://market.ft.tech/gateway';
|
|
34
|
-
/** 10 QPS 的 80% 上限:120ms 最小间隔 ⇒ 稳态 ~8.3 次/秒,突发也不会越过 20。 */
|
|
35
12
|
export const FTSHARE_MIN_INTERVAL_MS = 120;
|
|
36
|
-
/**
|
|
13
|
+
/** Daily flow accepts 1000. */
|
|
37
14
|
const MAX_PAGE_SIZE = 1000;
|
|
38
15
|
/**
|
|
39
|
-
* ⛔
|
|
40
|
-
*
|
|
41
|
-
* 两种都不许本层放宽——静默那一半尤其只能靠参数层先拦住,否则模型以为拿到了整页。
|
|
42
|
-
* 2026-10-16 补:大宗交易(传 1000/2000 回显 500)与龙虎榜明细(同形)也走这一档;
|
|
43
|
-
* 业绩预告 / 快报则是 501 → 响亮 400「page_size 不能超过 500」。**同一档里两种形状都有**。
|
|
16
|
+
* ⛔ Cap 500: daily K-line / forecast 501 → HTTP 400 `page_size must be between 1 and 500`; main-capital,
|
|
17
|
+
* block trades and dragon-tiger silently truncate 1000/2000 to 500. Never raise the local cap.
|
|
44
18
|
*/
|
|
45
19
|
const MAX_PAGE_SIZE_500 = 500;
|
|
46
|
-
/**
|
|
47
|
-
* ⛔ 申万行业日度指标那一处更低:实测 `page_size=1000` 回显 `pageSize=200` 且只回 200 行
|
|
48
|
-
* (201 也一样被截成 200),同样是**静默**截断。
|
|
49
|
-
* 批次 F 复用这一档:南华指数日 K 实测 201 → **响亮** 400「page_size must be between 1 and 200」,
|
|
50
|
-
* 同一条上限两种失败形状都有。
|
|
51
|
-
*/
|
|
20
|
+
/** Cap 200: SW industry silently truncates; Nanhua index K-line rejects 201. */
|
|
52
21
|
const MAX_PAGE_SIZE_200 = 200;
|
|
53
|
-
/**
|
|
22
|
+
/** Cap 100: index weights silently clamp 101 to 100. */
|
|
54
23
|
const MAX_PAGE_SIZE_100 = 100;
|
|
55
24
|
/**
|
|
56
|
-
* ⛔
|
|
57
|
-
*
|
|
58
|
-
* 东财持仓传 5000 → **响亮** 400「page_size must be between 1 and 500」;
|
|
59
|
-
* 贵金属传 1001 → 静默夹到 1000;涨跌停价压到 **50**——实测 SHFE 50 可过、60 起整页 400
|
|
60
|
-
* 「missing pre-settlement price」(缺前结算价的是上游自己的某些合约,不是我们的参数)。
|
|
25
|
+
* ⛔ Futures caps: warrant/weekly/settle silently clamp 5000→4000; eastmoney positions 400;
|
|
26
|
+
* bullion clamps 1001→1000; limit prices cap at 50 (SHFE 60+ → 400 "missing pre-settlement price").
|
|
61
27
|
*/
|
|
62
28
|
const MAX_PAGE_SIZE_4000 = 4000;
|
|
63
29
|
const MAX_PAGE_SIZE_50 = 50;
|
|
64
30
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* 所以映射表必须建在我们这一侧:模型从任何一颗拿到的代码都要能直接打到 K 线上。
|
|
69
|
-
* ⛔ `.IB`(银行间债券远期,清单里确有 64 只 `ADBC10_2612.IB`)换成长后缀也没有行情端——
|
|
70
|
-
* 实测 `.NIB` → 400,这一族上游不卖银行间行情,所以这里响亮拒绝而不是"试一下"。
|
|
31
|
+
* ⛔ Three futures suffix spellings: `.SHF/.CFE/.CZC/.GFE` in lists, `.CFX/.ZCE` in limit/settle,
|
|
32
|
+
* CTP full names in K-line (short suffix → 400). Map short forms to full names.
|
|
33
|
+
* `.IB` interbank forwards have no quote endpoint (`.NIB` → 400): reject.
|
|
71
34
|
*/
|
|
72
35
|
const FUTURES_SUFFIX = { SHF: 'SHFE', CFE: 'CFFEX', CFX: 'CFFEX', CZC: 'CZCE', ZCE: 'CZCE', GFE: 'GFEX' };
|
|
73
36
|
const FUTURES_LONG_SUFFIX = new Set(['SHFE', 'INE', 'DCE', 'CZCE', 'CFFEX', 'GFEX']);
|
|
74
|
-
/** 涨跌停价与结算参数这两颗的 `exchange` 是必填项(实测缺失 400「缺少必填参数:交易所」)。 */
|
|
75
37
|
const FUTURES_EXCHANGES = ['SHFE', 'INE', 'CZCE', 'CFFEX', 'DCE', 'GFEX'];
|
|
76
|
-
/**
|
|
38
|
+
/** Warrants cover commodity exchanges only; CFFEX publishes none. */
|
|
77
39
|
const WARRANT_EXCHANGES = ['SHFE', 'INE', 'CZCE', 'DCE', 'GFEX'];
|
|
78
|
-
/**
|
|
79
|
-
* ⛔ 东财持仓那一族的 `exchange` 与全仓其他地方**不是同一套写法**:实测 `GFEX` / `gfex` 都只回
|
|
80
|
-
* 0 行、不报错,真名是 `gfe`;而且整族大小写敏感(`AG` 0 行、`ag` 有数)。
|
|
81
|
-
* 本层把六家常规写法换成上游那一套小写,模型不需要知道 `gfe` 这个坑。
|
|
82
|
-
*/
|
|
40
|
+
/** ⛔ Eastmoney positions use lowercase and `gfe` for GFEX; `GFEX`/`gfex` return 0 rows, case-sensitive. */
|
|
83
41
|
const EASTMONEY_FUTURES_EXCHANGE = { SHFE: 'shfe', DCE: 'dce', CZCE: 'czce', CFFEX: 'cffex', INE: 'ine', GFEX: 'gfe', GFE: 'gfe' };
|
|
84
42
|
const FLOW_PATHS = {
|
|
85
43
|
stock: '/api/v1/market/data/ths-stock-daily-flow',
|
|
@@ -99,7 +57,7 @@ export class FtshareError extends Error {
|
|
|
99
57
|
this.code = details.code;
|
|
100
58
|
}
|
|
101
59
|
}
|
|
102
|
-
/**
|
|
60
|
+
/** One process-wide throttle: limits are per egress, two throttles = none. */
|
|
103
61
|
let sharedThrottle;
|
|
104
62
|
function ftshareThrottle(task) {
|
|
105
63
|
sharedThrottle ??= createRequestThrottle(FTSHARE_MIN_INTERVAL_MS);
|
|
@@ -108,11 +66,7 @@ function ftshareThrottle(task) {
|
|
|
108
66
|
function isRecord(value) {
|
|
109
67
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
110
68
|
}
|
|
111
|
-
/**
|
|
112
|
-
* 上游数值多处用**字符串**给(实测五档净流入 `"49285883.19"`、日 K 快照收盘价 `"11.5000"`)。
|
|
113
|
-
* 转成 number 是本层的职责:留在字符串里,data_junior 的 `filters` 数值比较会静默不命中。
|
|
114
|
-
* 空串/null/非数一律 `null`——不补 0(§9.5:空洞不许带出,`null` 与 `0` 语义不同)。
|
|
115
|
-
*/
|
|
69
|
+
/** Numbers arrive as strings; convert or data_junior numeric filters silently miss. Empty/NaN → null, never 0. */
|
|
116
70
|
function numberOrNull(value) {
|
|
117
71
|
if (value === undefined || value === null || value === '')
|
|
118
72
|
return null;
|
|
@@ -134,14 +88,13 @@ function date(value, name) {
|
|
|
134
88
|
throw new FtshareError('invalid_parameter', `parameter ${name} must be a valid YYYY-MM-DD date`);
|
|
135
89
|
return value;
|
|
136
90
|
}
|
|
137
|
-
/**
|
|
91
|
+
/** Upstream dates are `YYYYMMDD`; `2026-10-08` returns 400 `参数无效:date`. */
|
|
138
92
|
function compactDate(value) {
|
|
139
93
|
return value.replaceAll('-', '');
|
|
140
94
|
}
|
|
141
95
|
function dateMs(value) {
|
|
142
96
|
return Date.parse(`${value}T00:00:00+08:00`);
|
|
143
97
|
}
|
|
144
|
-
/** 上游日期列实测有两种写法:`2026-10-14` 与 `20261014`。归一成 ISO + 毫秒轴。 */
|
|
145
98
|
function normalizeDateCell(value) {
|
|
146
99
|
const raw = textOrNull(value);
|
|
147
100
|
if (raw === null)
|
|
@@ -152,10 +105,7 @@ function normalizeDateCell(value) {
|
|
|
152
105
|
const ms = dateMs(compact);
|
|
153
106
|
return { iso: compact, ms: Number.isNaN(ms) ? null : ms };
|
|
154
107
|
}
|
|
155
|
-
/**
|
|
156
|
-
* `ts_nanos` 是**纳秒**(实测 `1791963000000000000`)。本仓时间轴口径是毫秒(AGENTS.md §通用约定),
|
|
157
|
-
* 纳秒直接落进 Dataset 会让 data_junior 的区间过滤差六个数量级且不报错。
|
|
158
|
-
*/
|
|
108
|
+
/** `ts_nanos` is nanoseconds; the Dataset timeline is ms (6 orders of magnitude off otherwise). */
|
|
159
109
|
function nanosToMillis(value) {
|
|
160
110
|
const parsed = numberOrNull(value);
|
|
161
111
|
if (parsed === null)
|
|
@@ -163,14 +113,13 @@ function nanosToMillis(value) {
|
|
|
163
113
|
const ms = Math.round(parsed / 1e6);
|
|
164
114
|
return Number.isSafeInteger(ms) ? ms : null;
|
|
165
115
|
}
|
|
166
|
-
/** 毫秒 → 上海时区的自然日与切片时刻(A 股无夏令时,固定 +08:00 足够)。 */
|
|
167
116
|
function shanghaiParts(ms) {
|
|
168
117
|
const shifted = new Date(ms + 8 * 3600_000);
|
|
169
118
|
const iso = shifted.toISOString();
|
|
170
119
|
return { trade_date: iso.slice(0, 10), slice_time: iso.slice(11, 16) };
|
|
171
120
|
}
|
|
172
121
|
const SLICE_MINUTES = new Set([0, 15, 30, 45]);
|
|
173
|
-
/**
|
|
122
|
+
/** Intraday slices accept only the 15-minute grid (`0935` → 400). */
|
|
174
123
|
function sliceTime(value) {
|
|
175
124
|
const raw = typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';
|
|
176
125
|
const match = /^(\d{2}):?(\d{2})$/.exec(raw);
|
|
@@ -185,12 +134,7 @@ function sliceTime(value) {
|
|
|
185
134
|
throw new FtshareError('invalid_parameter', `parameter time ${raw} is off the 15-minute grid; use 0930/0945/1000/…`);
|
|
186
135
|
return `${hours}${minutes}`;
|
|
187
136
|
}
|
|
188
|
-
/**
|
|
189
|
-
* 分页参数。`page` 实测**生效**(2026-10-15 17:05 之后:报价族六页 × 1000 去重恰为 `total_items` 5574,
|
|
190
|
-
* 日 K 第十二页只剩 63 行,日度族 page=1/2/3 的首行分别是 000001 / 000007 / 000010)。
|
|
191
|
-
* 页码与页数一律**按请求自己算**、不读信封里的 `pageNum`/`pages`:实测回显与请求一致(page=1/2/3 逐页对齐),
|
|
192
|
-
* 但那是上游的状态;本层只从信封取 `total`,回显漂了也不会把错的页码写进 Dataset。
|
|
193
|
-
*/
|
|
137
|
+
/** Compute page/pages from the request; read only `total` from the envelope (echoed paging can drift). */
|
|
194
138
|
function pageParams(params, allowed, maxSize) {
|
|
195
139
|
for (const key of Object.keys(params))
|
|
196
140
|
if (!allowed.includes(key))
|
|
@@ -203,18 +147,13 @@ function pageParams(params, allowed, maxSize) {
|
|
|
203
147
|
throw new FtshareError('invalid_parameter', `parameter size must be an integer from 1 to ${maxSize}`);
|
|
204
148
|
return { page, size };
|
|
205
149
|
}
|
|
206
|
-
/**
|
|
207
|
-
* 信封有两套(实测):正常与 4xx 是 `{code,message,data}`,套餐外 403 是 `{code,data,msg}`。
|
|
208
|
-
* 只读 `message` 会把"你无权访问"报成"上游错误:undefined",白丢一句最该给用户看的话。
|
|
209
|
-
*/
|
|
150
|
+
/** Two envelopes: `{code,message,data}` normally, `{code,data,msg}` for plan 403; read both message and msg. */
|
|
210
151
|
function envelopeFailure(envelope, status) {
|
|
211
152
|
const code = numberOrNull(envelope.code);
|
|
212
153
|
const message = textOrNull(envelope.message) ?? textOrNull(envelope.msg) ?? 'request failed';
|
|
213
154
|
const reason = textOrNull(isRecord(envelope.data) ? envelope.data.reason : undefined);
|
|
214
155
|
const details = status === undefined ? {} : { status };
|
|
215
156
|
if (code === null) {
|
|
216
|
-
// 信封里没有业务码(例如 429 只回一个空 JSON 体):这时**只有 HTTP 状态还能说话**,
|
|
217
|
-
// 把它退化成 upstream_error 等于让限流看起来像"上游坏了",Agent 会去改参数而不是退避。
|
|
218
157
|
if (status === 429)
|
|
219
158
|
return new FtshareError('rate_limited', 'FTShare HTTP error 429', details);
|
|
220
159
|
if (status === 403)
|
|
@@ -235,7 +174,7 @@ function envelopeFailure(envelope, status) {
|
|
|
235
174
|
return new FtshareError('rate_limited', `FTShare API error 429: ${message}`, { ...details, code: 429 });
|
|
236
175
|
return new FtshareError('upstream_error', `FTShare API error ${code}: ${message}`, { ...details, code });
|
|
237
176
|
}
|
|
238
|
-
/**
|
|
177
|
+
/** Key resolved per call; constructing the client touches no credentials. */
|
|
239
178
|
export function createFtshareClient(resolveApiKey, baseUrl = FTSHARE_BASE_URL) {
|
|
240
179
|
return {
|
|
241
180
|
async getJson(path, search, signal) {
|
|
@@ -255,9 +194,7 @@ export function createFtshareClient(resolveApiKey, baseUrl = FTSHARE_BASE_URL) {
|
|
|
255
194
|
throw error;
|
|
256
195
|
throw new FtshareError('http_error', `FTShare request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
257
196
|
}
|
|
258
|
-
// ⛔
|
|
259
|
-
// `data.reason`("当前套餐无权访问该接口…")是给用户看的唯一人话;按状态先抛就等于把它丢了,
|
|
260
|
-
// 而 4xx 的 `response.json()` 在 fetch 里完全合法。
|
|
197
|
+
// ⛔ Read the envelope before status: 403/400 bodies carry `data.reason`, the only user-facing wording.
|
|
261
198
|
const parsed = await response.json().catch(() => null);
|
|
262
199
|
if (isRecord(parsed)) {
|
|
263
200
|
const failure = envelopeFailure(parsed, response.status);
|
|
@@ -267,7 +204,6 @@ export function createFtshareClient(resolveApiKey, baseUrl = FTSHARE_BASE_URL) {
|
|
|
267
204
|
throw new FtshareError('http_error', `FTShare HTTP error ${response.status}`, { status: response.status });
|
|
268
205
|
return parsed;
|
|
269
206
|
}
|
|
270
|
-
// 没有可解析的信封:只能按 HTTP 状态归类(429/403/400 的语义与上面保持一致)。
|
|
271
207
|
if (response.status === 429)
|
|
272
208
|
throw new FtshareError('rate_limited', 'FTShare HTTP error 429', { status: 429 });
|
|
273
209
|
if (response.status === 403)
|
|
@@ -281,15 +217,8 @@ export function createFtshareClient(resolveApiKey, baseUrl = FTSHARE_BASE_URL) {
|
|
|
281
217
|
};
|
|
282
218
|
}
|
|
283
219
|
/**
|
|
284
|
-
*
|
|
285
|
-
* -
|
|
286
|
-
* - daec 报价族、涨跌停价、股东人数:`data.items` + `total_items/total_pages`(**不回显页码**)
|
|
287
|
-
* - 指数权重:`data.index_weights` + `total/page/page_size`(没有 pages)
|
|
288
|
-
* - 昨收价与可转债 K 线:`data` **直接是数组**,那时分页完全由本层合成
|
|
289
|
-
* ⛔ `data` 还可能是 **`null`**:实测龙虎榜明细在"非交易日 / 未来日期 / 早于窗口"三种情况下都回
|
|
290
|
-
* `{"code":200,"message":"success","data":null}`——那是**合法空结果**而不是坏信封,
|
|
291
|
-
* 读成失败就等于让模型对着一个日历事实反复换参数重试。
|
|
292
|
-
* 只取 `total`(三个名字都要读,实测与逐日全市场计数对得上);页码不从信封取,见 `pageParams` 的 ⛔。
|
|
220
|
+
* Four envelope shapes: `data.records`, `data.items`, `data.index_weights`, or `data` as a bare array.
|
|
221
|
+
* ⛔ `data:null` is a legal empty result (non-trading day/future date), not an error; only `total` is trusted.
|
|
293
222
|
*/
|
|
294
223
|
function readPaged(envelope) {
|
|
295
224
|
const data = envelope.data;
|
|
@@ -308,23 +237,14 @@ function readPaged(envelope) {
|
|
|
308
237
|
const rows = raw.filter(isRecord);
|
|
309
238
|
return { rows, total: integerOrNull(data.total) ?? integerOrNull(data.total_items) ?? rows.length };
|
|
310
239
|
}
|
|
311
|
-
/** 分页信封:`page`/`pages` 按请求算,`total` 用上游的。 */
|
|
312
240
|
function pagedEnvelope(item, total, page, size) {
|
|
313
241
|
return { item, pagination: { page, size: item.length, pages: Math.max(1, Math.ceil(total / size)), total } };
|
|
314
242
|
}
|
|
315
|
-
/**
|
|
316
|
-
* 没有分页信封的响应(市场快照 / 一次取满的那几颗):一行就是全部。
|
|
317
|
-
* ⛔ `total` 允许另给:期货 K 线那种"limit 只回最新若干根、但上游 total 报的是区间内根数"的响应,
|
|
318
|
-
* 把 total 写成 item.length 等于替上游抹掉截断(批次 E 的"截断要看得见"同一条)。
|
|
319
|
-
*/
|
|
243
|
+
/** ⛔ Allow an explicit total: futures K-line limit returns the latest N but total counts the whole range. */
|
|
320
244
|
function snapshotEnvelope(item, total = item.length) {
|
|
321
245
|
return { item, pagination: { page: 1, size: item.length, pages: 1, total } };
|
|
322
246
|
}
|
|
323
|
-
/**
|
|
324
|
-
* 日期区间有没有被应用,只能在返回之后核:昨收价那一族的响应里**没有身份列**(只有 `date` 与数值),
|
|
325
|
-
* 本层能核的最强事实就是"回来的行是否都落在请求的区间内"。实测上游遵守区间(2022-01-01..2026-10-15 回的行
|
|
326
|
-
* 起于 2022-04-19、止于 2026-10-15),所以这一声是**防改版**:区间语义哪天变了,宁可失败也不落一份错日期的盘。
|
|
327
|
-
*/
|
|
247
|
+
/** Verify after the fact: rows outside the requested window mean upstream ignored the range. */
|
|
328
248
|
function assertInRange(rows, start, end, pick) {
|
|
329
249
|
if (rows.length === 0)
|
|
330
250
|
return;
|
|
@@ -337,12 +257,7 @@ function assertInRange(rows, start, end, pick) {
|
|
|
337
257
|
throw new FtshareError('invalid_response', `FTShare returned trade_date ${iso} outside the requested ${start}..${end} window — 上游没有应用日期区间,这一份不是该区间的结果`);
|
|
338
258
|
}
|
|
339
259
|
}
|
|
340
|
-
/**
|
|
341
|
-
* ⛔ 过滤有没有真的生效,只能在**返回之后**验。实测上游是遵守的(`code=600519` 五个交易日回 5 行、
|
|
342
|
-
* `board_name=银行` 回 1 行),但它的失败形状是静默的:不认识的**写法**不回错、只回 0 行(实测
|
|
343
|
-
* `symbol = "600519.XSHG"` 回 0 行),不认识的**列**才点名拒。所以核身份放在 producer:
|
|
344
|
-
* 过滤语义一旦漂了,拿一句 `invalid_response` 换掉"把全市场样本当成一只票的序列"。
|
|
345
|
-
*/
|
|
260
|
+
/** ⛔ Filter failures are silent (unknown spelling → 0 rows), so verify identity per row after fetch. */
|
|
346
261
|
function assertFiltered(rows, filter) {
|
|
347
262
|
if (filter === undefined || rows.length === 0)
|
|
348
263
|
return;
|
|
@@ -363,19 +278,14 @@ function query(params, allowed) {
|
|
|
363
278
|
}
|
|
364
279
|
return search;
|
|
365
280
|
}
|
|
366
|
-
/** 不分页的那几颗也要挡陌生键名(`pageParams` 顺手做了这件事,这里给它们同一句话)。 */
|
|
367
281
|
function assertKnown(params, allowed) {
|
|
368
282
|
for (const key of Object.keys(params))
|
|
369
283
|
if (!allowed.includes(key))
|
|
370
284
|
throw new FtshareError('invalid_parameter', `unsupported parameter: ${key}`);
|
|
371
285
|
}
|
|
372
|
-
/**
|
|
286
|
+
/** Futures code shape `product[digits].suffix`; unknown spellings return empty silently. */
|
|
373
287
|
const FUTURES_CODE_PATTERN = /^[A-Z][A-Z0-9_]*\d{3,4}(?:\.[A-Z]{2,5})?$/;
|
|
374
|
-
/**
|
|
375
|
-
* 合约代码归一:转大写、把短后缀换成 K 线要的 CTP 全称后缀。
|
|
376
|
-
* ⛔ 裸码(`A2611`)原样放行——实测上游确实认(219 行日 K),但它**没有交易所身份**,
|
|
377
|
-
* 本层不替它猜(同一串裸码在别家上游可能另有其合约)。
|
|
378
|
-
*/
|
|
288
|
+
/** Uppercase and map short suffix to CTP long name; bare codes pass through without guessing an exchange. */
|
|
379
289
|
function futuresSymbol(value, name = 'symbol') {
|
|
380
290
|
const raw = typeof value === 'string' || typeof value === 'number' ? String(value).trim().toUpperCase() : '';
|
|
381
291
|
if (!FUTURES_CODE_PATTERN.test(raw))
|
|
@@ -390,7 +300,6 @@ function futuresSymbol(value, name = 'symbol') {
|
|
|
390
300
|
throw new FtshareError('invalid_parameter', `parameter ${name} ${raw} 的后缀 ${suffix} 认不出:交易所后缀用 SHF/SHFE、CFE/CFX/CFFEX、CZC/ZCE/CZCE、GFE/GFEX、DCE、INE 这几家写法(清单与结算那几颗给的后缀长短不一,本层会换)`);
|
|
391
301
|
return `${base}.${long}`;
|
|
392
302
|
}
|
|
393
|
-
/** 涨跌停价与结算参数的 `ts_code`:实测短后缀与全称后缀**都能命中**(返回行一律写短后缀),所以只做形状与写法核。 */
|
|
394
303
|
function futuresTsCode(value, name = 'ts_code') {
|
|
395
304
|
const raw = typeof value === 'string' || typeof value === 'number' ? String(value).trim().toUpperCase() : '';
|
|
396
305
|
if (!FUTURES_CODE_PATTERN.test(raw) || !raw.includes('.'))
|
|
@@ -406,7 +315,7 @@ function futuresExchange(value, allowed, name = 'exchange') {
|
|
|
406
315
|
throw new FtshareError('invalid_parameter', `parameter ${name} must be one of ${allowed.join(' / ')}`);
|
|
407
316
|
return raw;
|
|
408
317
|
}
|
|
409
|
-
/**
|
|
318
|
+
/** Weekly period is `YYYYWW`; other formats return 400. */
|
|
410
319
|
function weekCode(value, name) {
|
|
411
320
|
const raw = typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';
|
|
412
321
|
if (!/^\d{6}$/.test(raw) || Number(raw.slice(4)) < 1 || Number(raw.slice(4)) > 53)
|
|
@@ -489,7 +398,6 @@ function createFlowSource(options) {
|
|
|
489
398
|
throw new FtshareError('invalid_parameter', 'parameter start_date must not be later than end_date');
|
|
490
399
|
const normalized = { start_date, end_date, page, size };
|
|
491
400
|
if (typeof params.code === 'string' || typeof params.code === 'number') {
|
|
492
|
-
// 参数层要求带后缀的完整代码(本仓通则),发给上游时按上游口径退回裸码。
|
|
493
401
|
normalized.code = parseSecurityCode(params.code, 'code').digits;
|
|
494
402
|
}
|
|
495
403
|
if (typeof params.board_name === 'string' && params.board_name.trim())
|
|
@@ -511,11 +419,9 @@ function createFlowSource(options) {
|
|
|
511
419
|
page_size: params.size,
|
|
512
420
|
}, ['start_date', 'end_date', 'code', 'board_name', 'page', 'page_size']);
|
|
513
421
|
const { rows, total } = readPaged(await options.client.getJson(options.path, search, signal));
|
|
514
|
-
// 带过滤的请求先核身份,再核日期区间(两道都是防改版,见 assertFiltered / assertInRange)。
|
|
515
422
|
assertFiltered(rows, params.code === undefined
|
|
516
423
|
? (params.board_name === undefined ? undefined : { field: 'board_name', expected: String(params.board_name), label: 'board_name' })
|
|
517
424
|
: { field: 'code', expected: String(params.code), label: 'code' });
|
|
518
|
-
// 第二道:没带过滤时身份核对不会跑,但"整段日期区间被忽略"仍然只能靠返回核。
|
|
519
425
|
assertInRange(rows, String(params.start_date), String(params.end_date), (row) => row.trade_date);
|
|
520
426
|
return { data: pagedEnvelope(rows.map(options.mapRow), total, Number(params.page), Number(params.size)), schema: output(options.row) };
|
|
521
427
|
},
|
|
@@ -557,14 +463,9 @@ function mapBoardFlowRow(row) {
|
|
|
557
463
|
leader_name: textOrNull(row.leader_name), leader_change_pct: numberOrNull(row.leader_change_pct), leader_price: numberOrNull(row.leader_price),
|
|
558
464
|
};
|
|
559
465
|
}
|
|
560
|
-
/** 追加在同一份日度说明后面的板块口径段(各自独立,避免复制整篇正文)。 */
|
|
561
466
|
const INDUSTRY_FLOW_TAIL = ' 本能力是**行业板块**粒度:实测单日 90 个板块(同花顺一级行业口径),company_count 是该板块公司家数,leader_name / leader_change_pct / leader_price 是上游给的领涨股三列(涨跌幅同为百分数原值)。⛔ 板块的 inflow_yi / outflow_yi **不能跨板块相加**去凑"全市场":实测 90 行 inflow 合计 4088.1 亿,与个股口径不可比,相加只会得出没有人认领的数。';
|
|
562
467
|
const CONCEPT_FLOW_TAIL = ' 本能力是**概念板块**粒度:实测单日 387 个概念。⛔ 概念**天然重叠**(实测「2026三季报预增」43 家与「2026中报预增」514 家含同一批票),所以概念之间、概念与行业之间的 inflow_yi / outflow_yi 禁止相加或做占比;"某板块净买入多少"只能在**同一板块内**读。';
|
|
563
|
-
/**
|
|
564
|
-
* key 每次调用现取(与 `resolveFuyaoApiKey` 同一口径):credentials 优先、环境变量兜底。
|
|
565
|
-
* ⛔ 注册面**不得**以"装配期读到 key"为条件(AGENTS.md §9.7),也不要把这里的 rejection 缓存住——
|
|
566
|
-
* 一次装配期抢跑会把这个能力钉到进程结束。
|
|
567
|
-
*/
|
|
468
|
+
/** Credentials first, env fallback; never gate registration on key availability or cache a rejection. */
|
|
568
469
|
export async function resolveFtshareApiKey(ctx, credentialRef = 'FTSHARE_API_KEY') {
|
|
569
470
|
const credentials = ctx.get('credentials');
|
|
570
471
|
if (credentials?.resolve) {
|
|
@@ -576,17 +477,12 @@ export async function resolveFtshareApiKey(ctx, credentialRef = 'FTSHARE_API_KEY
|
|
|
576
477
|
return processLike?.env?.[credentialRef];
|
|
577
478
|
}
|
|
578
479
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* 其余列(`st` / `status` / `listing_date` / `market_cap` / `pe_ttm` / `shares`)会被点名拒
|
|
582
|
-
* 「不支持的过滤器字段:st;支持的…」;身份还有两套写法(`symkey` 带后缀、`symbol_id` 裸 6 位),
|
|
583
|
-
* 写错不报错只回 0 行。本层只用**已验过回 1 行**的 `symbol_id = "<裸 6 位>"`,模型只看 `code` 参数。
|
|
584
|
-
* 2. `board` 是 **URL 路径段**,只允许 `all / xshg / xshe / bjse` 四个常量:实测越界一律 HTTP 400
|
|
585
|
-
* 「unsupported stock board ys; expected xshg/xshe/bjse/all」。任何写法都不把模型给的字符串拼进路径。
|
|
480
|
+
* ⛔ Quote filters expose only symkey/symbol_id/market_id/name and price/volume; `symbol_id` is bare
|
|
481
|
+
* 6 digits (wrong spellings → 0 rows). `board` is a URL path segment limited to all/xshg/xshe/bjse.
|
|
586
482
|
*/
|
|
587
483
|
const QUOTE_BOARDS = ['all', 'xshg', 'xshe', 'bjse'];
|
|
588
484
|
const SNAPSHOT_SCOPES = ['ChinaStock', 'Xshg', 'Xshe', 'Bjse'];
|
|
589
|
-
/**
|
|
485
|
+
/** Order-by whitelist; market_cap/pe_ttm/shares are rejected upstream. */
|
|
590
486
|
const QUOTE_SORT = { change_ratio: 'change_rate', turnover_yuan: 'turnover', volume_shares: 'volume', close: 'close', prev_close: 'prev_close' };
|
|
591
487
|
const quoteRow = {
|
|
592
488
|
type: 'object',
|
|
@@ -687,7 +583,7 @@ function createQuoteSource(client) {
|
|
|
687
583
|
validateOutput: (data) => isRecord(data) && Array.isArray(data.item) && isRecord(data.pagination),
|
|
688
584
|
execute: async (request, signal) => {
|
|
689
585
|
const params = normalize(request.params);
|
|
690
|
-
// ⛔ filter
|
|
586
|
+
// ⛔ Build the filter only from validated 6 digits; never pass model expressions through.
|
|
691
587
|
const filter = params.code === undefined ? undefined : `symbol_id = "${params.code}"`;
|
|
692
588
|
const search = query({ filter, order_by: params.order_by, page: params.page, page_size: params.size }, ['filter', 'order_by', 'page', 'page_size']);
|
|
693
589
|
const { rows, total } = readPaged(await client.getJson(`/api/v1/market/data/daec/stocks/${params.board}`, search, signal));
|
|
@@ -709,11 +605,7 @@ const breadthRow = {
|
|
|
709
605
|
},
|
|
710
606
|
required: ['scope', 'market_status'], additionalProperties: false,
|
|
711
607
|
};
|
|
712
|
-
/**
|
|
713
|
-
* 全市场快照(`/daec/market/snapshot`)。这一颗自带两道对账,2026-10-15 连打 8 次逐分一致:九档家数之和 =
|
|
714
|
-
* 报价面的标的总数(5574),且三个交易所 scope 在家数、成交额、成交量三种量上都能加出全市场那一档。
|
|
715
|
-
* 取回先核这两道加法——对不上就是上游改版(`turnover` 与家数分属不同列族,谁都可能先漂)。
|
|
716
|
-
*/
|
|
608
|
+
/** Snapshot self-checks: nine buckets sum to the quote universe; three scopes add up to the China total. */
|
|
717
609
|
function createBreadthSource(client) {
|
|
718
610
|
const capability = 'ftshare_market_breadth';
|
|
719
611
|
const schema = {
|
|
@@ -784,12 +676,7 @@ const klineSnapshotRow = {
|
|
|
784
676
|
},
|
|
785
677
|
additionalProperties: false,
|
|
786
678
|
};
|
|
787
|
-
/**
|
|
788
|
-
* 全市场某交易日日 K 截面(`/stock-candlesticks-daily`)。它与 `ftshare_ashare_quote` 是**两个时刻**:
|
|
789
|
-
* 这一条是"某一天收工后的每只票",可翻页取完(实测十二页 × 500 = 5563 行,末页只剩 63 行);
|
|
790
|
-
* 报价那条是"此刻"。两者行数差恰好等于当日停牌家数(实测 5574 − 11 = 5563),
|
|
791
|
-
* 这是本仓少见的能自我对账的地方,写进描述里让模型也拿它当校验点。
|
|
792
|
-
*/
|
|
679
|
+
/** End-of-day K snapshot for one date, pageable; quotes are "now". Row-count gap equals suspended stocks. */
|
|
793
680
|
function createKlineSnapshotSource(client) {
|
|
794
681
|
const capability = 'ftshare_daily_kline_snapshot';
|
|
795
682
|
const schema = {
|
|
@@ -838,7 +725,7 @@ function createKlineSnapshotSource(client) {
|
|
|
838
725
|
session_open_ms: numberOrNull(row.ts_millis_open), close_time_ms: closeMs,
|
|
839
726
|
};
|
|
840
727
|
}).filter((row) => row.trade_date_ms !== null);
|
|
841
|
-
// ⛔
|
|
728
|
+
// ⛔ Requested and returned date must match; there is no other identity check.
|
|
842
729
|
assertInRange(item, String(params.trade_date), String(params.trade_date), (row) => row.trade_date);
|
|
843
730
|
return { data: pagedEnvelope(item, total, Number(params.page), Number(params.size)), schema: output(klineSnapshotRow) };
|
|
844
731
|
},
|
|
@@ -884,7 +771,7 @@ function createPrevCloseSource(client) {
|
|
|
884
771
|
const end_date = date(params.end_date, 'end_date');
|
|
885
772
|
if (start_date > end_date)
|
|
886
773
|
throw new FtshareError('invalid_parameter', 'parameter start_date must not be later than end_date');
|
|
887
|
-
//
|
|
774
|
+
// Keep the key `code` (canonical value) so a second normalize pass does not reject it.
|
|
888
775
|
return { code: parseSecurityCode(params.code, 'code').canonical, start_date, end_date };
|
|
889
776
|
};
|
|
890
777
|
return {
|
|
@@ -893,7 +780,6 @@ function createPrevCloseSource(client) {
|
|
|
893
780
|
validateOutput: (data) => isRecord(data) && Array.isArray(data.item) && isRecord(data.pagination),
|
|
894
781
|
execute: async (request, signal) => {
|
|
895
782
|
const params = normalize(request.params);
|
|
896
|
-
// 上游要的是 canonical 短后缀身份(`600519.SH`,实测与裸 `600519` 等价),原样发已校验过的值。
|
|
897
783
|
const search = query({ symbol: params.code, since: compactDate(String(params.start_date)), until: compactDate(String(params.end_date)) }, ['symbol', 'since', 'until']);
|
|
898
784
|
const { rows } = readPaged(await client.getJson('/api/v1/market/data/daec/history/prev-closes', search, signal));
|
|
899
785
|
assertInRange(rows, String(params.start_date), String(params.end_date), (row) => row.date);
|
|
@@ -911,12 +797,8 @@ const calendarRow = {
|
|
|
911
797
|
additionalProperties: false,
|
|
912
798
|
};
|
|
913
799
|
/**
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
* (实测同一区间 cn 与 hk 的日期集合不同)。
|
|
917
|
-
* 2. 巡检不许编日期,而本族其他能力的日期都得是真交易日——本层把上游的 `20261015`(**数字**)
|
|
918
|
-
* 归一成 `2026-10-15`,下游能力与巡检都用这一套写法。
|
|
919
|
-
* ⛔ 返回后必须核日期落不落在区间内:这是这一族唯一可核的事实,上游改版时它比落一份错 Dataset 好。
|
|
800
|
+
* Calendar: range-capable and covers HK (Fuyao's is no-arg, one year, CN only). Upstream returns
|
|
801
|
+
* `20261015` as a number; normalize, then verify every date falls in the requested range.
|
|
920
802
|
*/
|
|
921
803
|
function createCalendarSource(client) {
|
|
922
804
|
const capability = 'ftshare_trading_calendar';
|
|
@@ -975,11 +857,7 @@ function createCalendarSource(client) {
|
|
|
975
857
|
},
|
|
976
858
|
};
|
|
977
859
|
}
|
|
978
|
-
|
|
979
|
-
* 这一节每一句"实测"都能在 2026-10-15 落盘的原始报文里逐字回查(本模块头部那条 ⛔ 的规矩):
|
|
980
|
-
* 报文与统计脚本在 `/tmp/ftshare-evidence/probe-c/`,结论表见 `docs/design/ftshare-source-survey.md` §7.7。
|
|
981
|
-
*/
|
|
982
|
-
/** 上游实测认的四档报告期;`q4` / `Q4` / `h1` 这类别名**不报错**(见那颗能力的 description),所以参数层只放这四值。 */
|
|
860
|
+
/** Only q1/q2/q3/annual; aliases like q4/h1 do not error upstream but are rejected here. */
|
|
983
861
|
const DISCLOSURE_REPORT_TYPES = ['q1', 'q2', 'q3', 'annual'];
|
|
984
862
|
const blockTradeRow = {
|
|
985
863
|
type: 'object',
|
|
@@ -1061,11 +939,6 @@ function mapExpressRow(row) {
|
|
|
1061
939
|
publish_date: iso, publish_date_ms: ms,
|
|
1062
940
|
};
|
|
1063
941
|
}
|
|
1064
|
-
/**
|
|
1065
|
-
* 业绩预告与业绩快报共用一颗工厂:路径、行形状与正文各给各的,**参数面完全一致**
|
|
1066
|
-
* (单票 modeA 不分页 / `year` + `report_type` 翻页,二选一),所以互斥检查、枚举白名单与
|
|
1067
|
-
* 防改版核对只写一遍(§9.7 同一族共用一份实现)。
|
|
1068
|
-
*/
|
|
1069
942
|
function createDisclosureSource(options) {
|
|
1070
943
|
const allowed = ['code', 'year', 'report_type', 'page', 'size'];
|
|
1071
944
|
const schema = {
|
|
@@ -1097,7 +970,7 @@ function createDisclosureSource(options) {
|
|
|
1097
970
|
const normalized = { page, size };
|
|
1098
971
|
if (params.code !== undefined) {
|
|
1099
972
|
if (params.year !== undefined || params.report_type !== undefined) {
|
|
1100
|
-
//
|
|
973
|
+
// Upstream silently switches to single-stock when both modes are given; reject instead.
|
|
1101
974
|
throw new FtshareError('invalid_parameter', 'parameter code 与 year/report_type 二选一,不能同时给');
|
|
1102
975
|
}
|
|
1103
976
|
normalized.code = parseSecurityCode(params.code, 'code').canonical;
|
|
@@ -1123,18 +996,14 @@ function createDisclosureSource(options) {
|
|
|
1123
996
|
validateOutput: (data) => isRecord(data) && Array.isArray(data.item) && isRecord(data.pagination),
|
|
1124
997
|
execute: async (request, signal) => {
|
|
1125
998
|
const params = normalize(request.params);
|
|
1126
|
-
//
|
|
1127
|
-
// (§10.7 幂等——第二遍归一化的输入就是第一遍的输出,改名会让它自己报 unsupported parameter)。
|
|
999
|
+
// Rename to `stock_code` only at request time so normalize stays idempotent.
|
|
1128
1000
|
const stockCode = params.code;
|
|
1129
|
-
// ⛔
|
|
1130
|
-
// 连 page=2 / page_size=10 都照样给全部),所以这里不把 page 发出去——发了等于对外宣称能翻页,
|
|
1131
|
-
// 而 `pages = ceil(total ÷ size)` 会把一份完整历史算成两页,模型翻第二页会拿到重复的一整份。
|
|
1001
|
+
// ⛔ Single-stock mode never paginates; no page params or synthesized pages (a second page repeats everything).
|
|
1132
1002
|
const search = query(stockCode !== undefined
|
|
1133
1003
|
? { stock_code: stockCode, year: params.year, report_type: params.report_type }
|
|
1134
1004
|
: { year: params.year, report_type: params.report_type, page: params.page, page_size: params.size }, ['stock_code', 'year', 'report_type', 'page', 'page_size']);
|
|
1135
1005
|
const { rows, total } = readPaged(await options.client.getJson(options.path, search, signal));
|
|
1136
|
-
//
|
|
1137
|
-
// ⛔ 核的是**上游原始列名**(stock_code / year / report_type),映射后的行已经把身份列改名了。
|
|
1006
|
+
// Filters fail silently; verify against raw upstream column names (stock_code/year/report_type).
|
|
1138
1007
|
if (stockCode !== undefined) {
|
|
1139
1008
|
assertFiltered(rows, { field: 'stock_code', expected: String(stockCode), label: 'code' });
|
|
1140
1009
|
return { data: snapshotEnvelope(rows.map(options.mapRow)), schema: output(options.row) };
|
|
@@ -1172,7 +1041,7 @@ function mapSwOverviewRow(row) {
|
|
|
1172
1041
|
dividend_yield_pct: numberOrNull(row.dvRatio), dividend_yield_percentile: numberOrNull(row.dvRatioPercentile),
|
|
1173
1042
|
};
|
|
1174
1043
|
}
|
|
1175
|
-
/**
|
|
1044
|
+
/** Upstream amount is thousands and total_mv 10k yuan; convert both to yuan. */
|
|
1176
1045
|
function toYuan(value, factor) {
|
|
1177
1046
|
const parsed = numberOrNull(value);
|
|
1178
1047
|
if (parsed === null)
|
|
@@ -1235,7 +1104,7 @@ function mapSwMetricsRow(row) {
|
|
|
1235
1104
|
amount_congestion: numberOrNull(row.amountCongestion), amount_congestion_quantile: numberOrNull(row.amountCongestionQuantile),
|
|
1236
1105
|
};
|
|
1237
1106
|
}
|
|
1238
|
-
/**
|
|
1107
|
+
/** SW industry code requires `.SI`; bare or unknown codes return total=0 without error. */
|
|
1239
1108
|
function swIndustryCode(value, name = 'industry_code') {
|
|
1240
1109
|
const raw = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
1241
1110
|
if (!/^\d{6}\.SI$/.test(raw)) {
|
|
@@ -1389,7 +1258,6 @@ function createBlockTradesSource(client) {
|
|
|
1389
1258
|
const search = query({ date: params.date === undefined ? undefined : compactDate(String(params.date)), page: params.page, page_size: params.size }, ['date', 'page', 'page_size']);
|
|
1390
1259
|
const { rows, total } = readPaged(await client.getJson('/api/v1/market/data/block-trades', search, signal));
|
|
1391
1260
|
const item = rows.map(mapBlockTradeRow);
|
|
1392
|
-
// 省略 date 时"最新的一天"由上游决定,没有可比的对端,只核对请求过的那一类。
|
|
1393
1261
|
if (params.date !== undefined)
|
|
1394
1262
|
assertInRange(item, String(params.date), String(params.date), (row) => row.trade_date);
|
|
1395
1263
|
return { data: pagedEnvelope(item, total, Number(params.page), Number(params.size)), schema: output(blockTradeRow) };
|
|
@@ -1398,12 +1266,6 @@ function createBlockTradesSource(client) {
|
|
|
1398
1266
|
}
|
|
1399
1267
|
const FORECAST_DESCRIPTION = '按报告期获取 A 股**业绩预告**(含北交所与 ST 票),一行是「一只票 × 一个报告期 × 一个预测科目」。两种查法互斥:给 `code`(须带市场后缀,本层转成上游要的 `000001.SZ` 写法)= 单票全部历史报告期且**不分页**(实测 000001.SZ 回 16 行、total=16、pages 恒 1);给 `year` + `report_type` = 该报告期全市场翻页(实测 2024 annual 3150 行、2023 annual 3333 行、2024 的 q1 229 / q2 1766 / q3 223 行)。⛔ 两组同时给会在参数层被拒——上游这时**静默只按单票查**(实测 code + year=2024 + q1 仍回那 16 行)。`report_type` 只认四值:实测上游另有**不报错的别名**(`q4` 与 `Q4` 都按 annual 查得 3150、`h1` 按 q2 查得 1766),本层把别名挡在门外;真正不认识的写法(`zzz`)上游才 400 点名拒「report_type 无效: zzz, 应为 q1/q2/q3/annual」。⛔ **少给任一个都只回 0 行、不回错**:只给 year(实测 total_items=0)、代码不带后缀(`000001`)、后缀写成聚宽写法(`600519.XSHG`)都是空结果。金额三列(`forecast_value_min` / `forecast_value_max` / `last_year_value`)的单位由同行的 `unit` 说明:实测 2025 annual 全市场逐页取满 3368 行里 `unit` **恒为「元」**;一旦看到别的取值,金额口径要跟着它改。`forecast_value_min == forecast_value_max` 是上游给了点值(多数行如此),不是缺失。⛔ **0 不是「预告为零」**:同一批里有 26 行上下限同为 0,其中 25 行的 yoy 仍非 0(今世缘 2025 营业收入 0/0、yoy 5~12)——那是**缺值编码**,请改看 `yoy_change_*` 与 `last_year_value`;另有 6 行连 `last_year_value` 都是 0。`yoy_change_min_pct` / `max_pct` 是**百分数原值**,实测口径 (预测 − 去年同期) ÷ |去年同期| × 100(平安银行 2015 年报:(20792060000 − 19801960000) ÷ 19801960000 = 5.0000 = 字段 5;上限 15.0000 = 字段 15)。⛔ **0 是「不适用」而不是「同比持平」**:实测 597 行有 190 行 yoy 记 0,这些行的去年同期或预测值至少一方为负(声迅股份 2024 由盈转亏、万林物流 2024 由亏转盈、退市鹏博 2024 减亏都记 0),而预测与去年同期的净利润**都为正**的 28 行 yoy 全部与上面那条算式相符——遇到 0 请自己用 forecast 与 last_year 算。`forecast_subject` 与 `forecast_type` 原样透出、本层不改写:实测 3368 行里科目只有三值(归属于上市公司股东的净利润 3235 / 营业收入 120 / 扣除非经常性损益后的净利润 13)、类型只有五值(预减 1414 / 预增 849 / 略增 585 / 略减 514 / 其他 6);供应商文档另列了 续亏 / 扭亏 / 续盈 等写法,本层不把任何一组当枚举。modeB 的排序实测是 **`publish_date` 递减**(前 1500 行逐行单调,实测首行发布 2026-04-30、第 1500 行 2026-01-29),所以"最新预告"跨页分散,要按票或按科目取最新一条请自己分组。年份深度:实测 2000 annual 有 93 行、2005 年 603 行、2025 年 3368 行(越近越多)。防改版:带 code 逐行核 `stock_code`,按报告期核 `year` 与 `report_type`;`size` 上限 500(实测 501 → HTTP 400「page_size 不能超过 500」)。';
|
|
1400
1268
|
const EXPRESS_DESCRIPTION = '按报告期获取 A 股**业绩快报**(已披露快报的核心财务指标),一行是「一只票 × 一个报告期」。查法与 `ftshare_performance_forecast` 完全一致(`code` 单票不分页 / `year` + `report_type` 全市场翻页,两组互斥、`report_type` 只认 q1/q2/q3/annual,越界写法参数层拒)。⛔ **快报是稀薄披露**:实测 2024 annual 全市场只有 1192 行(预告同口径 3150 行)、2024 q1 只有 8 行、2005 annual 104 行;什么都不给时上游回 total_items=0 且不报错,取到空是常态而不是失败。同一只票同一报告期可有**多条**更正披露:实测 2024 annual 首页 500 行里 477 个「票×年×期」组合中有 23 个占两行,两行的 `publish_date` 与数值都不同(ST帕瓦 2024:净利 −1,097,147,600 发布于 2025-02-28、−726,501,500 发布于 2025-04-30)。⛔ modeB 的排序实测是 **`publish_date` 递减**(500 行逐行单调),同一票的原披露与更正披露因此相距很远(实测那两条分别落在第 1 行与第 316 行)——要"最新口径"必须按 `publish_date` 分组取最大,**不能按行序或页序猜**。单票 modeA 实测是一期一行(000001.SZ 共 4 条 = 2018 / 2019 / 2021 / 2022 年报各一条)。单位与口径:`eps_yuan`(每股收益)、`net_assets_per_share_yuan`(每股净资产)、`total_revenue_yuan` / `net_profit_yuan` 及各自 `_prev` 上期值都由上游以字符串给出、单位元,本层转数值;`*_yoy_pct` 与 `roe_pct` 是**百分数原值**。⛔ **`net_profit_yoy_pct` 在上期亏损时符号是反的**:实测 2024 / 2023 / 2021 三个年度各取 500 行(共 1500 行)按 (本期 − 上期) ÷ |上期| × 100 逐行核——上期**为正**的 1260 行全部相符(平安银行 2022 年报:(45516000000 − 36336000000) ÷ 36336000000 × 100 = 25.2642 = 字段值),上期**为负**的 240 行**全部与该数符号相反**(ST帕瓦 2024:本期 −726,501,500、上期 −247,643,300 是亏损扩大,算得 −193.3661 而字段是 +193.3661;亚信安全 2024 扭亏,算得 +103.2949 而字段是 −103.2949)。⛔ 所以**别拿这个字段判方向**,要"亏损扩大还是收窄"请自己用本期与上期比。`total_revenue_yoy_pct` 在 499 行(上期均为正)内全部相符,没出现上期为负的样本,规律不外推。`roe_pct` 与同行其它列的算法关系**未核出**(股本要另取 `ftshare_ashare_quote` 的 `shares`,而那是当前股本、不是报告期股本)。⛔ `total_revenue_yoy_pct` 实测出现过 null(46 行里 1 行)——**null 不是 0**,也不是"没增长"。分页 `page` 实测生效,`size` 上限 500(实测 501 → HTTP 400「page_size 不能超过 500」;`page` 或 `size` 给 0 也 400「page/page_size 必须大于 0」,这条比预告那族更响)。防改版:带 code 逐行核 `stock_code`,按报告期核 `year` 与 `report_type`。';
|
|
1401
|
-
/**
|
|
1402
|
-
* 批次 D(2026-10-16 07:36–07:44 北京时间,176 份落盘报文):涨跌停价、龙虎榜席位、股东人数、
|
|
1403
|
-
* 指数成分权重、可转债历史 K 线。五颗都是逐条对照本仓现有能力后挑的**真缺口**,不是"同族再加一颗":
|
|
1404
|
-
* 涨跌停此前只有 `tencent_quote` 的当日口径且明确拒北证;席位此前任何一颗都没有;股东人数只有
|
|
1405
|
-
* 东财最新一期截面;成分权重只有名单没有数;转债只有发行条款没有价格序列。
|
|
1406
|
-
*/
|
|
1407
1269
|
const LIMIT_INSTRUMENT_TYPES = ['stock', 'etf', 'cb'];
|
|
1408
1270
|
const limitPriceRow = {
|
|
1409
1271
|
type: 'object',
|
|
@@ -1431,10 +1293,7 @@ function mapLimitPriceRow(row) {
|
|
|
1431
1293
|
};
|
|
1432
1294
|
}
|
|
1433
1295
|
const LIMIT_DESCRIPTION = '按交易日获取全市场或单只标的的**涨跌停价与昨收价**,一行是「一只标的 × 一个交易日」。补的两处空白都经过实测:本仓此前只有 `tencent_quote` 的**当日**涨跌停,而它在参数层明确拒北交所;这一条实测 2026-10-15 全市场 7587 行里含 **350 只 `.BJ`**,而且能翻历史(600519.SH 单票 2010-01-04 起有数,2009 全年 total=0)。两种查法:不给 `code` = 按 `date` 取该日全市场并翻页;给 `code`(须带市场后缀,本层归一成 `000001.SZ` 写法)= 单票,配 `date` 取一天、或配 `start_date` + `end_date` 取区间。⛔ 三条上游硬规则在参数层先拦,话术比它的 400 更具体:区间必须配 `code`(实测不带 code 只给区间 → 400「start_date/end_date 需配合 symbol 使用」)、`date` 与区间不可混用(实测 400「trade_date 不能与 start_date/end_date 同时使用」)、区间跨度不超过 3 年(实测 2020..2026 → 400「start_date 与 end_date 间隔不能超过 3 年」)。裸 6 位在上游是**按深市个股解释**的(实测 `000001` → 平安银行一行),本层不接受这种猜测、要求显式后缀;指数后缀会被上游点名拒(实测 `000001.SH` → 400「index has no limit price」)。`instrument_type` ∈ stock / etf / cb,实测过滤生效且**三类相加正好等于全市场**(2026-10-15:5576 + 1696 + 315 = total 7587),越界写法上游 400「instrument_type 仅支持 stock、etf、cb」。⛔ **`up_limit_yuan` / `down_limit_yuan` 不是交易所口径的精确价**:上游以字符串给最多 6 位小数,实测同日有 852/5576 只股票的涨停价带尾巴(000001.SZ 昨收 11.50 × 1.1 = 12.65,字段却是 12.649999),转债 259/315、ETF 1541/1696 更多(它们昨收本来就是 3~4 位)——要判"是否封板"请先按最小价格单位取整再比。幅度实测分桶(同日全市场):主板 ±10%、创业板与科创板 ±20%、北证 ±30%、转债 ±20%、ETF 多为 ±10%。⛔ 有 3 行的涨停价与跌停价**同时为 null**(920162.BJ / 301569.SZ / 301660.SZ),这三只在 2026-09-01..10-15 区间里**只出现在 10-15 这一天**,形状像上市首日不设限制,但成因未获对方确认——请按"无限制"而不是"数据缺失"读。非交易日与未来日期都只回 total=0、不报错(实测 20261011 周日、20270101)。防改版:带 `code` 逐行核身份,其余逐行核返回日期落在请求区间内。';
|
|
1434
|
-
/**
|
|
1435
|
-
* 每日涨跌停价(`/market/data/stk-limit`)。⛔ 这一颗的**列值不是交易所精确价**:实测 852/5576 只
|
|
1436
|
-
* 股票的涨停价是 `x.649999` 这种带尾巴的字符串,本层原样转数值、不做任何取整——取整口径属于分析层。
|
|
1437
|
-
*/
|
|
1296
|
+
/** ⛔ Limit prices are not exchange-exact (`x.649999`); pass through unrounded. */
|
|
1438
1297
|
function createLimitPriceSource(client) {
|
|
1439
1298
|
const capability = 'ftshare_limit_price';
|
|
1440
1299
|
const allowed = ['date', 'code', 'start_date', 'end_date', 'instrument_type', 'page', 'size'];
|
|
@@ -1469,7 +1328,6 @@ function createLimitPriceSource(client) {
|
|
|
1469
1328
|
const code = params.code === undefined ? undefined : parseSecurityCode(params.code, 'code').canonical;
|
|
1470
1329
|
const hasStart = params.start_date !== undefined;
|
|
1471
1330
|
const hasEnd = params.end_date !== undefined;
|
|
1472
|
-
// 实测上游对这三种混用都是**响亮 400**,本层先拦是为了把话说到"该改哪一个参数"这一层。
|
|
1473
1331
|
if ((hasStart || hasEnd) && code === undefined) {
|
|
1474
1332
|
throw new FtshareError('invalid_parameter', 'parameters start_date/end_date require code — 上游没有"全市场历史区间"这种查法(实测 400「start_date/end_date 需配合 symbol 使用」)');
|
|
1475
1333
|
}
|
|
@@ -1573,11 +1431,7 @@ function mapDragonTigerSeatRow(row, tradeDate) {
|
|
|
1573
1431
|
};
|
|
1574
1432
|
}
|
|
1575
1433
|
const SEATS_DESCRIPTION = '按**单个交易日**获取龙虎榜明细:一行是一只上榜票,带**买入额前五与卖出额前五的营业部席位**。这一层本仓此前没有——`dragon_tiger` 只给到票级榜单、`eastmoney_top_buy_sell_*` 只给汇总,都没有席位名字;⛔ 所以这三条口径不同,不能拿来互相核对条数。`date` 必填(YYYY-MM-DD,本层转成上游的 YYYYMMDD):上游允许省略(回"最新快照"),但**响应行里没有日期列**,省略时落库的行就没有时间轴,本能力因此要求显式日期、`trade_date` 由本层按请求补写。实测规模很小:2026-10-14 全日 68 行(SH 26 / SZ 37 / **BJ 5**)、2026-10-15 57 行、2023-01-03 44 行,`size` 给到 201 以上就**一页拿全**。⛔ 历史窗口实测起点在 2022 年初:2022-01-04 有 67 行,而 2021-12-31 与更早(2021-01-04 / 2020-01-02 / 2016-01-04 / 2012-01-04)都是 **0 行**,信封长这样 `{"code":200,"message":"success","data":null}`——那是合法空、不是取数失败;是滚动窗口还是固定起点未核出。非交易日与未来日期同样是 `data:null`(实测 20261011、20270101)。单位与口径:⛔ `change_ratio` 是**比例值不是百分数**(实测 002696.SZ 给 0.10045146726862288,与本仓 `ftshare_limit_price` 的昨收算 (9.75 ÷ 8.86 − 1) 差 3.2e-16),与日度资金流那族的 `change_pct` 不是一套、禁止直接比;`close_yuan` / `turnover_yuan` 与席位三列都是**元**(上游字符串,本层转数值)。`net_yuan = buy_yuan − sell_yuan` 实测 2026-10-14 的 68 行 × 10 席 = 680 条**违例 0**;⛔ 但同一天有 1 行的前五买入额合计**大于**当日成交额,所以席位金额与成交额不能互相闭合。`top_buyers` / `top_sellers` 通常各 5 条,上游缺名次时**不补位**(实测 2025-12-31 的 600343.SH 两边都只有 4 条),统计前先判长度。⛔ **更要紧的是:这两组不保证是营业部**。同一天的 600343.SH 那 4 条的 `name` 是**投资者分类**(机构 / 其他自然人 / 中小投资者 / 沪股通),`top_buyers` 与 `top_sellers` 两组内容**完全相同**,其中「机构」的 `buy_yuan` 是 67,487,960,000 而同行 `turnover_yuan` 只有 7,120,896,372.22——那是交易所按分类口径披露的行,**别按"前五营业部"解读,也别拿 buy ÷ turnover 算占比**。席位名是原文,「机构专用」是营业部口径里的一种取值。⛔ **不支持按票过滤**:实测把 `symbol` 发给上游不报错也不过滤(total 仍是全日 68 行),单票请整日取回后在 Dataset 里筛。`size` 的真实上限是 500 且超限**静默夹断**(文档写 200;实测传 1000 → 回显 pageSize=500)。';
|
|
1576
|
-
/**
|
|
1577
|
-
* 龙虎榜前五席位(`/market/data/abnormal-trading-details`)。
|
|
1578
|
-
* ⛔ `date` 是必填而不是可选:响应行内**没有日期列**(实测列集合就是 symbol/change_rate/close/turnover/
|
|
1579
|
-
* top_buyers/top_sellers),省略日期就等于落一份没有时间轴的 Dataset——这一条是本层加的,不是上游的限制。
|
|
1580
|
-
*/
|
|
1434
|
+
/** ⛔ `date` is required because rows carry no date column; without it the Dataset has no time axis. */
|
|
1581
1435
|
function createDragonTigerSeatsSource(client) {
|
|
1582
1436
|
const capability = 'ftshare_dragon_tiger_seats';
|
|
1583
1437
|
const allowed = ['date', 'page', 'size'];
|
|
@@ -1660,10 +1514,7 @@ function mapHolderNumberRow(row) {
|
|
|
1660
1514
|
};
|
|
1661
1515
|
}
|
|
1662
1516
|
const HOLDER_DESCRIPTION = '两种互斥查法:给 `code`(须带市场后缀)= 该票**全部历史**、一次给完不翻页(实测 000001.SZ 122 期回到 1995-12-31、600519.SH 107 期、920001.BJ 25 期);给 `is_last=true` = 全市场**各只票自己的最新一期**并翻页(实测 total_items=5926)。补的空白:本仓此前只有 `eastmoney_holder_number_snapshot` 的最新一期截面,**股东人数的历史序列一直是空的**。⛔ `is_last` **不是"同一报告期截面"**:实测同一页里 report_date 就有 2026-06-30 / 2026-09-30 / 2026-10-09 / 2026-03-31 好几种,做横切面统计请按 `report_date` 自己分组。两组参数同时给会被上游**静默只按单票查**(实测 code + is_last=true 仍回那 122 行),本层在参数层拒混用。写法敏感全是静默的:裸 6 位(`000001`)、聚宽后缀(`000001.XSHE`)、**小写后缀**(`600519.sh`)都回 0 行且不报错(实测),本层统一归一成 `600519.SH` 再发。⛔ **没有日期参数**:实测多发一个 `report_date` 上游不报错也不过滤(仍回 122 行),所以按报告期筛请取回全量后在 Dataset 里做,本层不提供也不会替它假装过滤。单位与口径:`holder_num` 是人;⛔ `holder_num_change_pct` 是**百分数原值**,口径 (本期 ÷ 上期 − 1) × 100 —— 实测 600519.SH 自 2002-06-30 起的 15 期逐期核对,与算式差 ≤1e-4;`ten_holder_pct` / `ften_holder_pct` 同为百分数(十大股东 / 十大流通股东持股比例,上游列名里的 `ften` 是它自己的拼写,本层不改名);`per_capita_circ_share` 是人均流通股(股),`per_capita_hold_amount_yuan` 是人均持股市值(元),`close_price_yuan` 元,`publish_date` / `report_date` 是 YYYY-MM-DD 原文并另有 `report_date_ms` 供时间轴。⛔ **`close_price_yuan` 的 0 不是价格**:实测 is_last 首页 500 行里 2 行为 0(如 603361.SH:股东数 27、报告期 2022-12-31),同行的 `per_capita_hold_amount_yuan` 会跟着是 0。整列为空是常态而不是坏数据:实测 600519.SH 的 107 期里 `per_capita_share_change_pct` 与 `chip_concentration` **107/107 全为 null**,`per_capita_circ_share` 83 期 null、`ten_holder_pct` 30 期 null、`ften_holder_pct` 17 期 null、`holder_num_change_ratio` 首期 1 行 null——⛔ null 不是 0,不要补值。`size` 上限 1000(实测 2000 / 5000 上游照给,但粗页会把一次调用变成几万行,本层不放)。防改版:带 `code` 逐行核 `stock_code` 与请求同一只票。';
|
|
1663
|
-
/**
|
|
1664
|
-
* 股东人数(`/holder/stock-holder-nums`)。⛔ 参数面**刻意没有日期**:实测上游收到多余的 `report_date`
|
|
1665
|
-
* 不报错也不过滤,那一句"我按报告期筛过了"是假的——要筛就取回全量在 Dataset 里筛。
|
|
1666
|
-
*/
|
|
1517
|
+
/** ⛔ No date parameter: upstream ignores extra report_date without filtering. */
|
|
1667
1518
|
function createHolderNumberSource(client) {
|
|
1668
1519
|
const capability = 'ftshare_holder_number';
|
|
1669
1520
|
const allowed = ['code', 'is_last', 'page', 'size'];
|
|
@@ -1691,7 +1542,6 @@ function createHolderNumberSource(client) {
|
|
|
1691
1542
|
};
|
|
1692
1543
|
const normalize = (params) => {
|
|
1693
1544
|
const code = params.code === undefined ? undefined : parseSecurityCode(params.code, 'code').canonical;
|
|
1694
|
-
// 实测 code + is_last=true 时上游静默只按单票查(total 仍是那 122 行),等于让模型以为筛过。
|
|
1695
1545
|
if (code !== undefined && params.is_last === true) {
|
|
1696
1546
|
throw new FtshareError('invalid_parameter', 'parameters code and is_last are mutually exclusive — 上游这时静默只按单票查(实测 total 仍是单票那 122 行),不会替你按 is_last 取全市场');
|
|
1697
1547
|
}
|
|
@@ -1712,8 +1562,7 @@ function createHolderNumberSource(client) {
|
|
|
1712
1562
|
validateOutput: (data) => isRecord(data) && Array.isArray(data.item) && isRecord(data.pagination),
|
|
1713
1563
|
execute: async (request, signal) => {
|
|
1714
1564
|
const params = normalize(request.params);
|
|
1715
|
-
// ⛔
|
|
1716
|
-
// 也不给 `pages = ceil(total ÷ size)`——那会把一份完整历史说成两页,翻页只会拿到重复的一整份。
|
|
1565
|
+
// ⛔ Single-stock mode never paginates; no page params and no synthesized pages.
|
|
1717
1566
|
const search = params.code === undefined
|
|
1718
1567
|
? query({ is_last: 'true', page: params.page, page_size: params.size }, ['is_last', 'page', 'page_size'])
|
|
1719
1568
|
: query({ stock_code: params.code }, ['stock_code']);
|
|
@@ -1727,7 +1576,7 @@ function createHolderNumberSource(client) {
|
|
|
1727
1576
|
},
|
|
1728
1577
|
};
|
|
1729
1578
|
}
|
|
1730
|
-
/** `is_last`
|
|
1579
|
+
/** `is_last` is the upstream string 'true'; only boolean true is allowed. */
|
|
1731
1580
|
function booleanParam(value) {
|
|
1732
1581
|
if (typeof value === 'boolean')
|
|
1733
1582
|
return value;
|
|
@@ -1759,10 +1608,6 @@ function mapIndexWeightRow(row) {
|
|
|
1759
1608
|
};
|
|
1760
1609
|
}
|
|
1761
1610
|
const INDEX_WEIGHT_DESCRIPTION = '按**中证系列指数**取某一采集期的成份股权重,一行是「一个指数 × 一个采集期 × 一只成分股」。补的是**权重数值**这一维度:`index_constituents` 给的是成分名单、不带权重,也不按采集期。`index_code` 必填,本层接受裸 6 位与 `000300.SH` / `399001.SZ` 这类带后缀写法并**统一成裸 6 位再发**——⛔ 上游对带后缀的写法是**静默回 0 行**、不报错(实测 `000300.SH` 与 `000001.SH` 都回 `index_weights:[]` 且 total=0),空值才是响亮 400「index_code 不能为空」,而不认识的写法(`ZZZZ`)同样只回 0 行。⛔ 后缀与号段矛盾(`000001.SZ`)在参数层拒:折成裸码就是上证综指,等于本层替你把一只深市身份换成另一个指数。⛔ 这里的 `000001` 是**上证综合指数**(实测 total=2230 只成分,首行 600000 浦发银行),不是平安银行——裸 6 位在行情类能力里通常指个股,本能力整个命名空间都是指数。实测条数与指数名正好对得上:000300 → 300 行、000905 → 500 行、932000 → 2000 行;而**申万行业码 `801010` 回 0 行**(行业口径请用 `ftshare_sw_industry_overview` / `ftshare_sw_industry_metrics`)。`date` 可选,是**采集期**且必须精确命中:实测 2026-06-15 与 2026-09-30 各回该期 300 行,而 2026-10-01 / 2026-01-01 这种没有采集的日期 total=0(**不会**回退到"最近一期"),省略时取最新一期(实测 2026-10-16 打 000300 的最新期是 2026-09-30)。⛔ `weight_pct` 是**百分数原值**,文档写的"小数字符串(如 0.0609)、非百分比"与实测不符:000300 最新期 300 行逐行相加 = **100.002**,单只形如 0.453000 就是 0.453%——别再乘 100。`component_code` 是**裸 6 位、不带交易所后缀**,本层不替它猜市场(需要后缀请用行情类能力或 `ticker_search`)。上游每行还有 `url_hash`(该期 xlsx 的下载 hash),本层不透出。`size` 上限 100,实测超限**静默夹断**(传 101 → 回显 page_size=100 并只回 100 行):宽指数务必按 `ceil(total ÷ size)` 翻页取满,只读第一页会把 2000 只当成 100 只。防改版:逐行核 `index_code` 与请求一致,带 `date` 时再核采集期等于请求。';
|
|
1762
|
-
/**
|
|
1763
|
-
* 中证指数成分权重(`/index/index_weight`)。
|
|
1764
|
-
* ⛔ 第四套信封:行数组叫 `data.index_weights`,分页字段是 `total/page/page_size`(**没有 pages**)。
|
|
1765
|
-
*/
|
|
1766
1611
|
function createIndexWeightSource(client) {
|
|
1767
1612
|
const capability = 'ftshare_index_weight';
|
|
1768
1613
|
const allowed = ['index_code', 'date', 'page', 'size'];
|
|
@@ -1818,13 +1663,7 @@ function createIndexWeightSource(client) {
|
|
|
1818
1663
|
},
|
|
1819
1664
|
};
|
|
1820
1665
|
}
|
|
1821
|
-
/**
|
|
1822
|
-
* ⛔ 与 `parseSecurityCode` 不是一回事:那一颗是给**个股/ETF**消歧的(裸 000xxx 在指数与个股之间
|
|
1823
|
-
* 有歧义所以它拒),而这里的整个命名空间就是指数——`000001` 在上游是上证综指(实测 2230 只成分)。
|
|
1824
|
-
* 带后缀的写法上游**静默回 0 行**(实测 `000300.SH` 与 `000001.SH` 都是 `index_weights:[]` 且 total=0),
|
|
1825
|
-
* 所以本层把合法后缀折成裸码再发;但后缀与号段互相矛盾的写法(`000001.SZ` 折过去会变成上证综指)
|
|
1826
|
-
* 一律在参数层拒——宁可不发,也不悄悄把用户写的身份换成另一个指数。
|
|
1827
|
-
*/
|
|
1666
|
+
/** ⛔ Indices: bare `000001` is the SSE Composite; suffixed input returns 0 rows. Strip valid suffixes, reject conflicts. */
|
|
1828
1667
|
function bareIndexCode(value, name = 'index_code') {
|
|
1829
1668
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
1830
1669
|
const match = /^(\d{6})(?:\.(SH|SZ|XSHG|XSHE))?$/i.exec(raw);
|
|
@@ -1873,11 +1712,7 @@ function mapCbKlineRow(row, thscode) {
|
|
|
1873
1712
|
};
|
|
1874
1713
|
}
|
|
1875
1714
|
const CB_KLINE_DESCRIPTION = '按**单只可转债**与日期区间取历史 K 线(日 / 周 / 月 / 年),一行一根 K,按时间升序。补的是硬空白:本仓此前只有 `eastmoney_convertible_bond_list` 的**发行条款与要素**,转债的**价格序列此前为零**。`code` 必填(6 位,裸码或带 `.SH` / `.SZ` 都收;实测裸码与带正确后缀**逐字节相同**,113042 = 113042.SH、127045 = 127045.SZ),⛔ 非转债代码与"后缀和代码不配对"都由上游点名拒(实测 600519.SH → 400「symbols [600519.XSHG] are not convertible bonds」;113042.SZ → 400「symbols [113042.XSHE] are not convertible bonds」),所以身份核对是上游做的、本层**不按代码首位猜交易所**(沪市转债是 11xxxx,2007-2009 还有 126 段老沪债,套个股那套自然市场规则会判错)。`start_date` / `end_date` 必填,本层按北京时间 +08:00 转成上游的 `since_ts_millis`(当日 00:00)与 `until_ts_millis`(当日 23:59:59)。⛔ **跨度上限是 12 个自然月,而且边界卡在"整整 12 个月"之外**:实测 2025-10-16..2026-10-15(364 天)正常回 242 行,而 2025-10-15..2026-10-15(365 天)被 400 拒「日 K 及以上周期查询范围不能超过 12 个自然月」——本层**不把它换算成天数闸门**(换算规则没核出,写成常量就是造一个会腐烂的闸门),超限时把上游这句话原样交回。`period` ∈ day / week / month / year(实测上游大小写不敏感,`DAY` 也收);⛔ `adjust` ∈ none / forward / backward,而这一列上游**大小写敏感**(文档写"大小写不敏感",实测发 `NONE` 与 `Forward` 都被 400 拒),所以本层统一转小写再发、写法不用操心。⛔ 本层**不宣称 forward / backward 就是前复权 / 后复权**:实测 113042.SH 在 2026-08-01..08-14 三种写法返回逐字节相同(该券该窗口没有复权事件),要原始行情核对请显式给 `none`。时间与单位:`ts_millis` 是这根 K 的**结束**时刻(实测日档落在北京时间 15:00)、`ts_millis_open` 是开始时刻(09:30),`trade_date` / `trade_date_ms` 由本层按 +08:00 从 `ts_millis` 推出;四档价格是元(上游字符串 4 位小数,本层转数值)、`turnover_yuan` 是元;⛔ `volume_tickets` 的单位是**张**(1 张面值 100 元)而不是手:实测同行 `turnover ÷ volume` 落在 [low, high] 区间内(113042.SH 2026-09-01..10-15 的 27 行违例 0,比值 117.824),按"手"乘 1000 会把它推出价格区间三个数量级。实测这 27 行 OHLC 自洽(high ≥ max(open,close) ≥ min(open,close) ≥ low)违例 0,且**八列都没有 null**。`limit` 可选,保留**最新若干根**(实测 limit=3 恰等于不传 limit 那一次的末三行);⛔ `limit=0` 回 0 行且不报错,本层要求正整数。周 / 月 / 年是**按请求区间内的日 K 聚合**,窗口没盖满一个周期时那根不是完整周期(实测 year 档在 2026-01-01..08-14 只给 1 根)。`interval_value` 实测对日档不改变结果(给 2 仍是每个交易日一根),本层不提供。当日日 K 收盘后即有(实测 2026-10-15 打当天回 1 行)。⛔ 响应 `data` **直接是数组**、没有分页信封,本层合成的 pagination 就是"一次取满"。各券历史起点以其实际上市与上游入库为准,本层未逐券核。';
|
|
1876
|
-
/**
|
|
1877
|
-
* 可转债历史 K 线(`/convertible-bond-candlesticks`)。
|
|
1878
|
-
* ⛔ 复权口径只透出不解释:实测 none / forward / backward 在无复权事件的券上逐字节相同,
|
|
1879
|
-
* 把它们写成"前复权 / 后复权"就是没有依据的断言(本族批次 B 那一课)。
|
|
1880
|
-
*/
|
|
1715
|
+
/** ⛔ Adjustment is passed through, not interpreted: none/forward/backward are identical for bonds without events. */
|
|
1881
1716
|
function createCbKlineSource(client) {
|
|
1882
1717
|
const capability = 'ftshare_convertible_bond_kline';
|
|
1883
1718
|
const allowed = ['code', 'period', 'adjust', 'start_date', 'end_date', 'limit'];
|
|
@@ -1933,7 +1768,6 @@ function createCbKlineSource(client) {
|
|
|
1933
1768
|
normalized.end_date = end_date;
|
|
1934
1769
|
if (params.limit !== undefined) {
|
|
1935
1770
|
const limit = Number(params.limit);
|
|
1936
|
-
// 实测 limit=0 不报错、只回 0 行;那会被读成"这只券没有行情"。
|
|
1937
1771
|
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
1938
1772
|
throw new FtshareError('invalid_parameter', 'parameter limit must be an integer >= 1(实测 0 静默回 0 行、负数被上游 400 拒)');
|
|
1939
1773
|
normalized.limit = limit;
|
|
@@ -1958,8 +1792,7 @@ function createCbKlineSource(client) {
|
|
|
1958
1792
|
if (!Array.isArray(envelope.data))
|
|
1959
1793
|
throw new FtshareError('invalid_response', 'FTShare CB kline envelope is not an array — 这一族没有分页信封,读不到数组就无法判断是不是空结果');
|
|
1960
1794
|
const rows = envelope.data.filter(isRecord);
|
|
1961
|
-
// ⛔
|
|
1962
|
-
// 本层能核的最强事实是时间:回来的每根 K 都必须落在请求的区间内。
|
|
1795
|
+
// ⛔ No identity column in rows; verify every K-line date falls in the requested range.
|
|
1963
1796
|
assertInRange(rows, String(params.start_date), String(params.end_date), (row) => {
|
|
1964
1797
|
const ms = numberOrNull(row.ts_millis);
|
|
1965
1798
|
return ms === null ? null : shanghaiParts(ms).trade_date;
|
|
@@ -1969,14 +1802,7 @@ function createCbKlineSource(client) {
|
|
|
1969
1802
|
},
|
|
1970
1803
|
};
|
|
1971
1804
|
}
|
|
1972
|
-
/**
|
|
1973
|
-
* 可转债代码。⛔ **不按代码首位猜交易所**(`parseSecurityCode` 那套自然市场规则在这儿是错的:
|
|
1974
|
-
* 它会把 11xxxx 判成 SZ,而沪市转债正是 11xxxx,还有 2007-2009 的 126 段老沪债)。
|
|
1975
|
-
* 实测上游自己会把写法折成内部 symbol 再判类别:`113042.SZ` 与 `127045.SH` 这种**后缀与代码不配对**
|
|
1976
|
-
* 的输入是响亮 400 点名(「symbols [113042.XSHE] are not convertible bonds」),不会静默解析成别的证券;
|
|
1977
|
-
* 裸 6 位与带正确后缀的写法实测**逐字节相同**(113042 = 113042.SH、127045 = 127045.SZ)。
|
|
1978
|
-
* 所以本层只做写法归一,身份与类别判断交给上游那句 400。
|
|
1979
|
-
*/
|
|
1805
|
+
/** ⛔ Never infer exchange from digits (11xxxx/126 are Shanghai CBs); upstream 400s mismatched suffixes. */
|
|
1980
1806
|
function convertibleBondCode(value, name = 'code') {
|
|
1981
1807
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
1982
1808
|
const match = /^(\d{6})(?:\.(SH|SZ|BJ|XSHG|XSHE))?$/i.exec(raw);
|
|
@@ -1987,15 +1813,7 @@ function convertibleBondCode(value, name = 'code') {
|
|
|
1987
1813
|
const market = suffix === 'XSHG' ? 'SH' : suffix === 'XSHE' ? 'SZ' : suffix;
|
|
1988
1814
|
return market === '' ? digits : `${digits}.${market}`;
|
|
1989
1815
|
}
|
|
1990
|
-
/**
|
|
1991
|
-
* 批次 E(2026-10-16 08:25–08:29 与 08:44 两轮,109 份落盘报文 + 18 份官方文档页):LPR、中债收益率曲线、SHIBOR、HIBOR、财经日历。
|
|
1992
|
-
* 五颗都是**整块空白**:本仓此前没有任何收益率曲线、没有任何拆借利率、没有 LPR 的免费通道
|
|
1993
|
-
* (只有 Wind EDB 那一条按次计费的),也没有发布时刻表一类的日历。
|
|
1994
|
-
* ⛔ 同一族里暂缓两颗,理由都写在报文里而不是文档里:`libor_daily` 的行只有
|
|
1995
|
-
* `currency` / `tenor` / `rate`、**没有基准名列**,LIBOR 停发之后 USD 隔夜到底是 SOFR 还是别的读不出来;
|
|
1996
|
-
* `us_economic` 的读数逐 `type` 单位上游不给、月份又是中文标签,锚不住输出契约。
|
|
1997
|
-
* `baidu_financial_calendar` 实测是 A 股事件提醒(股东大会 / 分红 / 停复牌),**不是见闻那棵的第二家**。
|
|
1998
|
-
*/
|
|
1816
|
+
/** LPR, bond yields, SHIBOR/HIBOR, calendar. Skipped: libor_daily (no benchmark), us_economic (units missing). */
|
|
1999
1817
|
const LPR_DESCRIPTION = '一次性返回 **LPR(贷款市场报价利率)全历史**,一行一个日期,⛔ **无参数、无分页**:实测多发 `start_date` / `page` / `page_size` 上游不报错也不过滤(回的仍是同一份 2661 行),要区间请在 Dataset 里按 `trade_date` 筛——本层因此不给这些座位。返回**按日期降序**(实测首行 2026-09-20、末行 2013-10-25)。⛔ **行粒度前后不一致**:2013-10-25..2020-12-20 是**逐自然日填充**(含周末;实测 2016-06 整月每天都是 4.3000),2020-12-21 起只在每月报价日给一行(实测日位集中在 20 / 21 / 22 / 24,自 2020-12-21 含当日起共 70 行)。⛔ 所以**别把行数当报价次数**:全程 2661 行里 `lpr_1y_pct` 只有 **28 个不同取值**、28 次变化;要"报价历史"请按值变化分组或每月取一条。`lpr_5y_pct` 在 2019-08-20 之前**整列为 null**(实测 2125 / 2661 行为 null,首个非空就是 2019-08-20 的 4.8500),⛔ null 不是 0。单位是**百分数原值**(3.0000 读作 3.00%)——依据是上游文档口径与量级,不是本层算出来的;上游以 4 位小数字符串给出,本层转数值。`trade_date_ms` 是北京零点。这一颗是给"贷款利率基准 / 房贷定价"用的免费第二家:此前本仓只能走 Wind EDB(按次计费)。';
|
|
2000
1818
|
const lprRow = {
|
|
2001
1819
|
type: 'object',
|
|
@@ -2023,7 +1841,7 @@ function createLprSource(client) {
|
|
|
2023
1841
|
output_schema: output(lprRow),
|
|
2024
1842
|
};
|
|
2025
1843
|
const normalize = (params) => {
|
|
2026
|
-
//
|
|
1844
|
+
// Upstream silently ignores all parameters; reject rather than pretend filtering.
|
|
2027
1845
|
const keys = Object.keys(params);
|
|
2028
1846
|
if (keys.length > 0)
|
|
2029
1847
|
throw new FtshareError('invalid_parameter', `unsupported parameter: ${keys[0]} — 这颗上游收到任何参数都静默忽略(实测多发 start_date / page / page_size 仍回同一份 2661 行),要区间请在 Dataset 里按 trade_date 筛`);
|
|
@@ -2132,11 +1950,7 @@ function createChinabondYieldSource(client) {
|
|
|
2132
1950
|
},
|
|
2133
1951
|
};
|
|
2134
1952
|
}
|
|
2135
|
-
/**
|
|
2136
|
-
* SHIBOR / HIBOR 同形(`records` 信封 + 一行一个交易日的多期限宽表),共用一颗工厂:
|
|
2137
|
-
* ⛔ 两者的期限档**不一样**(SHIBOR 八档含 `y1`、HIBOR 十五档到 `m12`),所以 `tenors` 必须由调用方给、
|
|
2138
|
-
* 不能猜——列名与实测报文逐字对齐,多给一档就会把 null 编成"这档存在"。
|
|
2139
|
-
*/
|
|
1953
|
+
/** SHIBOR (8 tenors incl. y1) and HIBOR (15 to m12) differ; callers supply the exact tenor list. */
|
|
2140
1954
|
const RATE_TENOR_ROW = (tenors) => ({
|
|
2141
1955
|
type: 'object',
|
|
2142
1956
|
properties: {
|
|
@@ -2257,8 +2071,7 @@ function createFinancialCalendarSource(client) {
|
|
|
2257
2071
|
if (start_date > end_date)
|
|
2258
2072
|
throw new FtshareError('invalid_parameter', 'parameter start_date must not be later than end_date(实测上游同样 400「结束时间不能早于开始时间」)');
|
|
2259
2073
|
const spanDays = Math.round((Date.parse(`${end_date}T00:00:00Z`) - Date.parse(`${start_date}T00:00:00Z`)) / 86400_000);
|
|
2260
|
-
//
|
|
2261
|
-
// 实测 3 天差通过、4 天差 400「时间范围不能超过3天」。
|
|
2074
|
+
// Fixed upstream contract: max 3-day span; 4 days returns 400 时间范围不能超过3天.
|
|
2262
2075
|
if (spanDays > 3)
|
|
2263
2076
|
throw new FtshareError('invalid_parameter', `parameter start_date..end_date spans ${spanDays} days — 上游单次窗上限是 3 天差(实测 400「时间范围不能超过3天」),请分段请求`);
|
|
2264
2077
|
return { start_date, end_date, page, size };
|
|
@@ -2276,7 +2089,7 @@ function createFinancialCalendarSource(client) {
|
|
|
2276
2089
|
const { rows, total } = readPaged(await client.getJson('/api/v1/market/data/finance/financial-calendar/wallstreetcn', search, signal));
|
|
2277
2090
|
const item = rows.map((row) => {
|
|
2278
2091
|
const { iso, ms } = normalizeDateCell(row.stat_date);
|
|
2279
|
-
//
|
|
2092
|
+
// `event_time` is a seconds string; convert or the timeline is off by 1000x.
|
|
2280
2093
|
const seconds = numberOrNull(row.event_time);
|
|
2281
2094
|
const millis = seconds === null ? null : Math.round(seconds * 1000);
|
|
2282
2095
|
const parts = millis === null ? null : shanghaiParts(millis);
|
|
@@ -2299,11 +2112,6 @@ function createFinancialCalendarSource(client) {
|
|
|
2299
2112
|
},
|
|
2300
2113
|
};
|
|
2301
2114
|
}
|
|
2302
|
-
// ── 批次 F:期货与贵金属(2026-10-16 逐条真报文,免费档 11 颗里选 10 颗)───────────────
|
|
2303
|
-
// ⛔ 这一批全部按"先档结构、再打报文、最后落字"的顺序做,证据在 /tmp/ftshare-evidence/probe-f/
|
|
2304
|
-
// (raw* / summary* / stats10 / crosscheck10)与 docs/design/ftshare-source-survey.md §7.11。
|
|
2305
|
-
// 暂缓的那颗是 eastmoney_futures_strange(龙虎榜):它一次回 12 个指标、三种载荷形状,
|
|
2306
|
-
// 落成行集需要"一行变多行"的展平,而 hub 的 mapRow 一行只出一行——为它先改 hub 不属于这一批。
|
|
2307
2115
|
const futuresContractRow = {
|
|
2308
2116
|
type: 'object',
|
|
2309
2117
|
properties: {
|
|
@@ -2318,7 +2126,6 @@ const futuresContractRow = {
|
|
|
2318
2126
|
additionalProperties: false,
|
|
2319
2127
|
};
|
|
2320
2128
|
const FUTURES_CONTRACTS_DESCRIPTION = '按交易日获取**全部在册期货合约清单**(六家商品与金融期货交易所 + 银行间),一行一只合约。不传 `trade_date` = 上游按 CST 取前一交易日。⛔ **行数不是合约数**:实测 2026-10-15 回 1812 行,里面只有 **906 个不同 `symbol`**——每只合约出现两份,两份**除 `future_type` 外逐列相同**(906 只逐个键比过),一份写 `2`、一份写 `null`。要一份干净清单请按 `symbol` 去重;按行计数会把合约数翻倍。`future_type` 在这份快照里实测只出现 `2` 与 `null`(各 906 行),文档口径的 1 主力 / 3 连续**一个都没有** ⇒ 不许把 `null` 读成"主力"。交易所分布实测(同一交易日):CFFEX 56 / CZCE 486 / DCE 500 / GFEX 96 / INE 134 / NIB 64 / SHFE 476。⛔ `NIB` 是银行间债券远期(`ADBC10_2612.IB` 一类),**清单里有它、行情端没有它**:实测换成 `.NIB` 打 K 线 → 400「unsupported suffix \'NIB\'」,那 64 只在别处取不到数,不是本仓漏接。`symbol` 用的是**短后缀**(`.SHF` / `.CFE` / `.CZC` / `.GFE`),K 线那颗要全称后缀,本层按交易所映射,不用你手工换。⛔ 采集时点:实测 2026-10-16 上午打 `trade_date=20261016` → **400「当日期货数据将在上海时间 20:20 后开放查询」**,那是合法的入库时刻、不是取数故障,不要重试(同一族里另有两颗打当天是 200 + total=0,见 `ftshare_futures_limit_price`)。**这一颗没有分页**:实测多发 `page_size=10` 仍回全量 1812 行,分页座位是假的,本层在参数层拒。1812 行一次给完必然超一次工具返回预算(实测原始报文 177,957 字节),会回 `too_large` + `profile_ref`——那是快照的正常用法,请转 `query_dataset` 用 `filters`(`exchange` / `product`)与 `columns_of_interest` 收窄。';
|
|
2321
|
-
/** 中国期货列表(`futures-lists`):按交易日的合约清单快照,⛔ 无分页。 */
|
|
2322
2129
|
function createFuturesContractsSource(client) {
|
|
2323
2130
|
const capability = 'ftshare_futures_contracts';
|
|
2324
2131
|
const allowed = ['trade_date'];
|
|
@@ -2357,7 +2164,7 @@ function createFuturesContractsSource(client) {
|
|
|
2357
2164
|
const params = normalize(request.params);
|
|
2358
2165
|
const envelope = await client.getJson(path, query({ trade_date: params.trade_date === undefined ? undefined : compactDate(String(params.trade_date)) }, ['trade_date']), signal);
|
|
2359
2166
|
const { rows } = readPaged(envelope);
|
|
2360
|
-
//
|
|
2167
|
+
// No row date; trade date appears once in `data.trade_date`, so fill the timeline here.
|
|
2361
2168
|
const { iso, ms } = normalizeDateCell(isRecord(envelope.data) ? envelope.data.trade_date : undefined);
|
|
2362
2169
|
const item = rows.filter((row) => row.symbol !== undefined && row.symbol !== null).map((row) => ({
|
|
2363
2170
|
symbol: String(row.symbol), exchange: textOrNull(row.exchange), product: textOrNull(row.product),
|
|
@@ -2400,7 +2207,6 @@ const futuresDetailRow = {
|
|
|
2400
2207
|
additionalProperties: false,
|
|
2401
2208
|
};
|
|
2402
2209
|
const FUTURES_DETAIL_DESCRIPTION = '按交易日获取**期货合约要素**(交易单位、合约乘数、最小变动价位、保证金、上市与摘牌日、交割方式),可按单只合约筛。与 `ftshare_futures_contracts` 同一套快照口径:⛔ 实测同一交易日 1812 行、只有 906 只合约,每只两份、两份除 `future_type` 外逐列相同。`symbol` 传标准合约全码(实测 `AG2610.SHF` 回 2 行、**大小写不敏感**:`ag2610.shf` 同样命中;⛔ 认不出的代码不回错、只回空 `items`,实测 `ZZZZ9999.SHF` → 200 + 0 行)。不带 `symbol` 时一次给全量要素,实测原始报文 **1,364,023 字节**,必然超一次工具返回预算并回 `too_large`——请带 `symbol`,或用 `query_dataset` 的 `filters` / `columns_of_interest` 收窄。⛔ 上游 28 列里本层只透出 24 列,**丢弃的列与理由**(按实测 1812 行的空值统计):`price_unit`(取值 1~1000、口径未披露)、`mean_cal_value`(只有 1 与 2)、`type_code` / `sub_type_code`(703xxx 内部码)、`value_info`(1756/1812 为空串)、`position_limit_info`(1806/1812 为空串)、`last_trade_date_hour`(1660/1812 为空串)。⛔ **两列装的是规则文字不是日期**:`delivery_date` 实测写「合约到期月份的第三个周五,遇法定节假日顺延(非完整周)」、`last_trade_date` 实测写「合约月份的15日(遇国家法定节假日顺延…)」,本层改名成 `delivery_date_rule` / `last_trade_date_rule`,让它们不会被当成日期列(§1.3:日期串落到数值时间列必须响亮失败,那前提是它得是个日期)。真日期是 `list_date`(实测 20231201~20261015)与 `de_list_tdate`(实测 20261015~20290831),都归一成 ISO + 毫秒轴;⛔ `de_list_tdate` 的最小值就是本次快照那天,说明这张表**带着当天摘牌的合约**,别拿它筛"在册"。单位:`trade_unit` 实测七种(张 / 吨 / 立方米 / 克 / 桶 / 手 / 千克),`quote_unit` 是报价单位文字(实测「人民币元/吨」「人民币元/千克」「指数点」),`multiplier` 是每手对应的交易单位数(实测 1~20000,72/1812 为 null)——⛔ 它与 `ftshare_futures_kline` 的 `vwap_per_lot_yuan`(每手成交额)是同一条算式的两侧:每手成交额 ≈ 价格 × multiplier。`spot_symbol` 只有 32/1812 行有值(股指期货那几只给现货指数代码,实测 IC → `000905.SH`)。';
|
|
2403
|
-
/** 中国期货基础数据(`futures-base-data`):合约要素快照,可按单只筛。⛔ 只取口径可承诺的列(§7.11)。 */
|
|
2404
2210
|
function createFuturesContractDetailSource(client) {
|
|
2405
2211
|
const capability = 'ftshare_futures_contract_detail';
|
|
2406
2212
|
const allowed = ['trade_date', 'symbol'];
|
|
@@ -2450,8 +2256,7 @@ function createFuturesContractDetailSource(client) {
|
|
|
2450
2256
|
const { iso, ms } = normalizeDateCell(isRecord(envelope.data) ? envelope.data.trade_date : undefined);
|
|
2451
2257
|
const snapshotDate = iso ?? (params.trade_date === undefined ? null : String(params.trade_date));
|
|
2452
2258
|
const snapshotMs = ms ?? (params.trade_date === undefined ? null : dateMs(String(params.trade_date)));
|
|
2453
|
-
//
|
|
2454
|
-
// 否则用户写小写、行里是大写,会把一份正确返回判成"过滤没生效"。
|
|
2259
|
+
// Symbol filter is case-insensitive upstream; compare uppercased row values.
|
|
2455
2260
|
if (params.symbol !== undefined) {
|
|
2456
2261
|
assertFiltered(rows.map((row) => ({ ...row, symbol: String(row.symbol ?? '').toUpperCase() })), {
|
|
2457
2262
|
field: 'symbol', expected: String(params.symbol), label: 'symbol',
|
|
@@ -2501,11 +2306,7 @@ const futuresKlineRow = {
|
|
|
2501
2306
|
const FUTURES_INTERVALS = ['daily', 'weekly', 'monthly', 'quarterly'];
|
|
2502
2307
|
const FUTURES_KLINE_DESCRIPTION = '按**单个合约**与日期区间取历史 K 线(日 / 周 / 月 / 季),一行一根 K、按时间**升序**。⛔ 交易所后缀这一课:清单 / 要素 / 结算 / 涨跌停四颗给的都是短后缀(`AG2610.SHF`),而这一颗只认 CTP **全称**后缀,实测短后缀 400 点名「unsupported suffix \'SHF\', only CTP suffixes (.SHFE, .INE, .DCE, .CZCE, .CFFEX, .GFEX) are supported」——本层按 `.SHF→.SHFE`、`.CFE→.CFFEX`、`.CZC→.CZCE`、`.GFE→.GFEX` 自动换(DCE / INE 原样),六家各拿真实合约核过、每家回 20 行;大小写实测不敏感(`cu2612.SHFE` 与 `CU2612.SHFE` 返回同一份 198 行),本层统一转大写。裸码(实测 `A2611` 回 219 行)也收,但它**没有交易所身份**、本层不替它猜。⛔ `interval` 只有这四档,其余一律在参数层拒——上游这一族是**静默**的:实测 `1d` / `1w` / `week` 回 **200 + 0 行**,只有 `day` / `1M` / `1Q` / `60m` / `1h` / 空串才 400「unsupported futures period interval」;供应商文档写的"兼容 1d / 1w / 1mo / 1q"与实测相反,放行 `1d` 等于把"写法不认"伪装成"这段时间没行情"。⛔ 日期只收 `YYYY-MM-DD`:上游要的是**毫秒**时间戳,实测传 `20260906` 这种 YYYYMMDD 整数 → 200 + 0 行 + total=0,所以本层按北京时间 +08:00 换成当日零点与 23:59:59,不给裸整数留座位。⛔ 跨度上限是 12 个自然月,本层**不换算成天数闸门**(换算规则没核出):实测 300 天的窗正常(194 根)、365 天与 399 天都被 400「日 K 及以上周期查询范围不能超过 12 个自然月」拒——更长序列请分段取。⛔ 周 / 月 / 季是**由区间内日 K 现聚合**的,窗口没盖满一个周期时那一根不是完整周期——这条核到过逐列等式:末根周 K(trade_date 20261015)的 `volume` 与 `amount` **正好等于**窗口内 10-12/13/14/15 那四天的和(180,070 手 / 98,888,888,750 元,一分不差),`open` = 四天里第一天的开盘、`close` = 最后一天的收盘、`open_interest` = 最后一天的持仓。`start_date` 单独给可以(实测 198 根),`end_date` 单独给上游 400(原话「end cannot be provided without start」),都不给 = 最新 `limit` 根(实测 limit=20 回末 20 根,而 `pagination.total` 仍是区间内根数 198)。`limit` 的默认值文档写 500(本层没核省略时到底给几根),实测传 5000 照给、区间内 198 根全给不截断;⛔ `limit=0` 回 **1 行**且 total 仍报 198(不报错),负数 400「请求参数格式错误:limit: invalid digit found in string」,所以本层要求正整数。单位与口径:`bar_time_ms` 是这根 K 的**结束**时刻(实测日档落在北京时间 15:00),`trade_date` / `trade_date_ms` 用上游那一列(YYYYMMDD 整数)归一而来;四档价格是合约报价单位(元/吨、元/千克、指数点,见 `ftshare_futures_contract_detail` 的 `quote_unit`);`volume_lots` 与 `open_interest_lots` 是**手**;`turnover_yuan` 是元;⛔ `vwap_per_lot_yuan` **不是价格**,是每手成交额:实测 194 行**全部**满足 `vwap ≡ amount ÷ volume`,与铜的合约乘数交叉核对——末行 545,566 元/手 ÷ 5 吨/手 = 109,113 元/吨,同日收盘 108,290 元/吨(差 0.76%,是加权均价与收盘价的差)。上游另有 `dominant_contract` / `forward_factor` / `backward_factor` 三列,实测那 194 行**全为 null**(文档也明写当前接口返回 null),本层不收录,因此**这颗不提供前复权 / 后复权**,跨合约拼接请自己按 `ftshare_futures_contracts` 的口径决定。当日行情:实测 2026-10-16 上午打当天区间 → 200 + 0 行(不是清单那颗那句 400),未来区间同样空——两种都是合法空。信封是 `data.items` + `data.total`、**没有分页**(`limit` 是唯一的取满手段),`pagination.total` 用上游那个数,这样 `limit` 截断看得见。';
|
|
2503
2308
|
const FUTURES_KLINE_PATH = '/api/v1/market/data/futures/kline';
|
|
2504
|
-
/**
|
|
2505
|
-
* 期货合约历史 K 线(`futures/kline`)。
|
|
2506
|
-
* ⛔ 除区间外还核一次身份:响应行里的 `symbol` 是**去后缀转小写**的写法(实测请求 `CU2612.SHFE`
|
|
2507
|
-
* 回行 `cu2612`),所以比的是剥掉交易所后缀再转小写的那一半,不是整串相等。
|
|
2508
|
-
*/
|
|
2309
|
+
/** ⛔ Row symbol is suffix-stripped lowercase (`CU2612.SHFE` → `cu2612`); verify after stripping. */
|
|
2509
2310
|
function createFuturesKlineSource(client) {
|
|
2510
2311
|
const capability = 'ftshare_futures_kline';
|
|
2511
2312
|
const allowed = ['symbol', 'interval', 'start_date', 'end_date', 'limit'];
|
|
@@ -2552,7 +2353,7 @@ function createFuturesKlineSource(client) {
|
|
|
2552
2353
|
throw new FtshareError('invalid_parameter', 'parameter start_date must not be later than end_date');
|
|
2553
2354
|
if (params.limit !== undefined) {
|
|
2554
2355
|
const limit = Number(params.limit);
|
|
2555
|
-
//
|
|
2356
|
+
// limit=0 returns 1 row while total reports the range size; reject.
|
|
2556
2357
|
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
2557
2358
|
throw new FtshareError('invalid_parameter', 'parameter limit must be an integer >= 1(实测 0 静默回 1 行、负数被上游 400 拒)');
|
|
2558
2359
|
normalized.limit = limit;
|
|
@@ -2587,7 +2388,7 @@ function createFuturesKlineSource(client) {
|
|
|
2587
2388
|
vwap_per_lot_yuan: numberOrNull(row.vwap), open_interest_lots: numberOrNull(row.open_interest),
|
|
2588
2389
|
};
|
|
2589
2390
|
});
|
|
2590
|
-
// ⛔ total
|
|
2391
|
+
// ⛔ total from upstream (range count), not item.length, so limit truncation is visible.
|
|
2591
2392
|
return { data: snapshotEnvelope(item, total), schema: output(futuresKlineRow) };
|
|
2592
2393
|
},
|
|
2593
2394
|
};
|
|
@@ -2608,7 +2409,6 @@ const futuresLimitRow = {
|
|
|
2608
2409
|
additionalProperties: false,
|
|
2609
2410
|
};
|
|
2610
2411
|
const FUTURES_LIMIT_DESCRIPTION = '按**一家交易所**获取期货合约的每日涨停价与跌停价(`exchange` 实测必填:不给就 400「缺少必填参数:交易所」;六家写法大小写不敏感,实测 `shfe` 与 `SHFE` 都回同一个 total=301)。日期两模式二选一:`trade_date` 单日,或 `start_date` + `end_date` 区间(实测区间有效:AG2610 在 2026-10-06..10-15 回 6 个交易日)。⛔ 行内 `ts_code` 的后缀拼法**随交易所而不随合约**:SHFE / DCE / GFEX / INE 分别是 `AG2610.SHF` / `A2611.DCE` / `LC2610.GFE` / `BC2610.INE`,而中金所写 `.CFX`(`IC2610.CFX`)、郑商所写 `.ZCE`(`AP610.ZCE`)——与清单那两颗(`.CFE` / `.CZC`)又不是一套;发给本层时长短拼法都能命中(实测 `AG2610.SHFE` 也命中,返回行仍写短后缀),要打到 K 线请用 `ftshare_futures_kline`,两种拼法本层都认。⛔ **分页上限压在 50,比文档写的 200 低得多**:实测 SHFE 2026-10-15 全天 301 行,`page_size` 20 与 50 可过,**60 起整页 400「missing pre-settlement price」**(100 / 500 / 1000 / 1500 / 2000 全是这一句),而 CZCE 传 51、100 都正常(total=243)、DCE 传 50 正常(total=232)——失败的不是页宽,是上游 SHFE 数据里某些合约**缺前结算价**;恢复手段是把页压回 50 以内逐页取(本层默认就锁在 50,模型不必自己试),不要换日期重试。`up_limit_price` / `down_limit_price` 是**价格**(合约报价单位,与 `ftshare_futures_settle` 同量纲):实测按 `ts_code` 与结算参数对上号之后,`up_limit ÷ settle` 在 248 行样本里落在 **1.0388~1.2706**(SHFE 1.056~1.271、DCE 1.048~1.114、CZCE 1.039~1.140、GFEX 1.080~1.214、INE 1.070~1.239)、`down_limit ÷ settle` 在 SHFE 那 50 行是 **0.8395~0.9750**——同量纲,所以这两个数可以直接与结算价对照着读。`m_ratio_pct` 是**百分数原值**(12 = 12%):文档写"最低交易保证金率,单位 %",实测它是 `ftshare_futures_settle` 里**套保两档的较低值 × 100**——五所合计 248 行**逐行完全吻合**(AD2612:投机 0.10、套保 0.09、m_ratio 9),而它**不等于投机档**(GFEX 48 行里只有 26 行等于多头投机、INE 50 行里只有 10 行)。⛔ 但 `m_ratio_pct` **不能用来复原涨跌停价**:`settle × (1 + m_ratio ÷ 100)` 与 `up_limit` 实测 248 行里只有 11 行吻合(SHFE 与 INE 各 0 行),因为涨跌停用的是**前结算价**与交易所当天的比例,不是这一档保证金率——不要拿它推算涨跌幅度。中金所那 28 行没法做这套交叉核对:实测 `ftshare_futures_settle` 对 CFFEX **回 0 行**(见那颗)。当日:实测 2026-10-16 打 `trade_date=20261016` → **200 + total=0**(与 `ftshare_futures_contracts` 那句 400 不同),收盘前拿到空页是合法的。行按 `trade_date` **降序**给(实测区间模式首行 20261015、第五行 20261009),"最近一天"在第一页。品种过滤 `cont` 实测生效(AG → total=12)。';
|
|
2611
|
-
/** 期货合约涨跌停价(`futures/ft-limit`):`exchange` 必填 + 单日或区间二选一。 */
|
|
2612
2412
|
function createFuturesLimitPriceSource(client) {
|
|
2613
2413
|
const capability = 'ftshare_futures_limit_price';
|
|
2614
2414
|
const allowed = ['exchange', 'trade_date', 'start_date', 'end_date', 'ts_code', 'cont', 'page', 'size'];
|
|
@@ -2634,7 +2434,7 @@ function createFuturesLimitPriceSource(client) {
|
|
|
2634
2434
|
ts_code: { type: 'string', description: '单只合约过滤(短后缀或全称后缀都实测命中)' },
|
|
2635
2435
|
cont: { type: 'string', description: '品种代码过滤(如 AG),实测生效' },
|
|
2636
2436
|
page: { type: 'integer', minimum: 1 },
|
|
2637
|
-
// ⛔ 50
|
|
2437
|
+
// ⛔ 50 is the real cap; SHFE fails from 60 despite docs saying 200.
|
|
2638
2438
|
size: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE_50 },
|
|
2639
2439
|
},
|
|
2640
2440
|
required: ['exchange'], additionalProperties: false,
|
|
@@ -2690,7 +2490,7 @@ function createFuturesLimitPriceSource(client) {
|
|
|
2690
2490
|
}
|
|
2691
2491
|
assertFiltered(rows, { field: 'exchange', expected: String(params.exchange), label: 'exchange' });
|
|
2692
2492
|
if (params.ts_code !== undefined) {
|
|
2693
|
-
//
|
|
2493
|
+
// Upstream accepts long suffixes but returns short ones; verify the bare symbol.
|
|
2694
2494
|
const bare = String(params.ts_code).split('.')[0];
|
|
2695
2495
|
assertFiltered(rows.map((row) => ({ ...row, ts_code: String(row.ts_code ?? '').split('.')[0] })), { field: 'ts_code', expected: bare, label: 'ts_code' });
|
|
2696
2496
|
}
|
|
@@ -2729,7 +2529,7 @@ const futuresSettleRow = {
|
|
|
2729
2529
|
additionalProperties: false,
|
|
2730
2530
|
};
|
|
2731
2531
|
const FUTURES_SETTLE_DESCRIPTION = '按**一家交易所**获取期货合约的每日结算参数(结算价、四档保证金率、手续费)。`exchange` 必填(实测缺失 400「缺少必填参数:交易所」)。⛔ **中金所这一颗没有数据**:实测 2026-10-15 与 10-08..10-14 区间、`CFFEX` 与 `CFX` 两种写法都回 **200 + total=0**——中金所的保证金与手续费本层取不到,那不是参数错、重试也没用(中金所的涨跌停请用 `ftshare_futures_limit_price`,实测 28 行有数)。日期同样二选一:`trade_date` 单日或 `start_date` + `end_date` 区间,⛔ **区间跨度上限 31 天**(实测 2026-09-10..10-15 → 400「start_date and end_date must be at most 31 days apart」;这一条文档与实测一致,本层在参数层拦下,把上游原话带进错误信息)。`size` 上限 **4000**(实测传 5000 静默夹到 4000、回显 `pageSize: 4000`,超限不报错,所以只能在参数层拦)。`ts_code` 实测短后缀与全称后缀都能命中(`AG2610.SHF` 与 `AG2610.SHFE` 同样 total=6),返回行写的是各所自己的拼法(SHFE `.SHF`、DCE `.DCE`、GFEX `.GFE`、INE `.INE`、郑商所 `.ZCE`)。单位与口径:`settle_price` 是结算价(合约报价单位——实测 2026-10-15 SHFE 那 301 行 min 521.3 / max 377140,量纲随合约本身),它是与 `ftshare_futures_limit_price` 交叉核对的那一侧(实测同一天按 `ts_code` 对上 248 行、`up_limit ÷ settle` 全部落在 1.04~1.27)。四档保证金率(`long_margin_rate` / `short_margin_rate` / `b_hedging_margin_rate` / `s_hedging_margin_rate`)实测落在 **0.06~0.22**,按**小数比例**读(0.22 = 22%);⛔ 投机两档与套保两档**是两回事**(实测 AD2612 投机 0.10、套保 0.09),涨跌停那颗的 `m_ratio_pct` 实测等于**套保两档的较低值 × 100**(五所 248 行逐行吻合),别拿它当投机保证金。⛔ `trading_fee_rate` / `trading_fee` / `delivery_fee` / `offset_today_fee` 这四列**单位上游没披露、本层也没核出来**(实测同一交易所里 trading_fee_rate 取 0~0.6、offset_today_fee 取 0.005~10、trading_fee 取 0~20、delivery_fee 取 0~2):是百分数还是千分数无法判定,所以**原样透出、不加单位后缀、不参与换算**——要算交易成本请用别的能力核过之后再写结论。数值列在上游全是**字符串**,本层转成数值、空值转 null 不补 0。当日:实测 2026-10-16 打 `trade_date=20261016` → **200 + total=0**(不是清单那颗那句 400),20:20 之前拿到空页是合法的。';
|
|
2732
|
-
/**
|
|
2532
|
+
/** Settle params: 31-day span max. */
|
|
2733
2533
|
function createFuturesSettleSource(client) {
|
|
2734
2534
|
const capability = 'ftshare_futures_settle';
|
|
2735
2535
|
const allowed = ['exchange', 'trade_date', 'start_date', 'end_date', 'ts_code', 'page', 'size'];
|
|
@@ -2848,7 +2648,7 @@ const warrantRow = {
|
|
|
2848
2648
|
additionalProperties: false,
|
|
2849
2649
|
};
|
|
2850
2650
|
const WARRANT_DESCRIPTION = '按交易日获取**商品交易所仓单日报**明细,一行是「一个品种 × 一家仓库」。日期三选一即可:`trade_date` 或 `start_date` + `end_date`(实测空参 400「证券代码、交易日期、开始日期或结束日期至少需要提供一个」——本层同样要求至少给一个条件,否则一次要拉全市场几年的行)。`exchange` 只五家(SHFE / INE / CZCE / DCE / GFEX,实测中金所不发仓单),`symbol` 是品种代码(实测 CU 配 SHFE 回 32 行、过滤生效)。`size` 上限 **4000**(实测 5000 静默夹到 4000 并回显;一天全市场实测 1469 行,一页就能取完)。⛔ **`unit` 随行而变,这是这张表最容易读错的一列**:实测 2026-10-15 一天里 CZCE 那 105 行是「张」、SHFE 那 32 行(铜)是「吨」、DCE 全部 193 行是「手」、GFEX 86 行是「手」,而 **INE 那 47 行同时有「吨」29 行与「桶」18 行**——同一家交易所内部就有两种单位。任何"仓单总量"都必须先按 `unit` 分组成员再相加;跨品种、跨所直接相加得到的是一个没有人认领的数。⛔ **DCE 有「小计」行的坑**:实测那 193 行里 49 行的 `futures_name` 带「小计」后缀(`豆一小计` 与 `豆一` 在同一品种 `A` 下并存),而两组的**仓库集合互不重叠**(实测 A / LH / V 三组重叠 0 家仓库:豆一 12 家 vs 豆一小计 9 家),所以「小计」不是把明细再加一遍、而是另一批仓库的名字;但按 `symbol` 分组求和会把这两种视图混在一起——要"某品种仓单"请**按 `futures_name` 分组**并看清它带不带「小计」。`warrant_change = warrant_volume − prev_warrant_volume` 实测 100 行**0 违例**,变动列可信。⛔ 列位随交易所而不随数据缺口:实测 DCE 193 行的 `warehouse_id` / `area` / `place` / `year_label` / `grade` / `brand` **整列为 null**;CZCE 那 105 行反过来只有 `area` 与 `brand` 全 null;SHFE 32 行只有 `area` 有值(实测「上海」);GFEX 86 行只有 `brand` 有 28 行——null 是"这一所不披露这一列"。上游另有 `pd` 与 `is_ct` 两列,实测五所合计 **463 行全为 null**、含义无法核出,本层不收录。当日:实测 2026-10-16 打 `trade_date=20261016` → 200 + total=0(合法的空,20:20 之前本就还没有当日仓单)。';
|
|
2851
|
-
/**
|
|
2651
|
+
/** ⛔ Warrant quantities must be grouped by `unit` before comparing. */
|
|
2852
2652
|
function createWarrantSource(client) {
|
|
2853
2653
|
const capability = 'ftshare_futures_warrant';
|
|
2854
2654
|
const allowed = ['trade_date', 'start_date', 'end_date', 'symbol', 'exchange', 'page', 'size'];
|
|
@@ -2968,14 +2768,14 @@ const futuresWeeklyRow = {
|
|
|
2968
2768
|
additionalProperties: false,
|
|
2969
2769
|
};
|
|
2970
2770
|
const FUTURES_WEEKLY_DESCRIPTION = '按统计周期获取**期货主要品种交易周报**,一行是「一周 × 一个品种」(实测一周整表 90 行、六家交易所都在)。条件至少给一个:`week` 单周、`prd` 品种(支持逗号分隔,实测 `CU,AL` 命中两个品种)、或 `start_week` + `end_week` 区间(实测空参 400「周、品种代码、开始周或结束周至少需要提供一个」;实测 `prd=CU` 单独给可用,一个品种 total=92 周、`page` 生效)。⛔ 周码一律 `YYYYWW`:实测 `2026-10-16` 这种写法 400「start_week must use YYYYWW format」,本层在参数层按同一个形状拦。`size` 上限 **4000**(实测 5000 静默夹到 4000)。⛔ **这张表有发布滞后,别按"本周"打**:实测 2026-10-16 打 202636..202642 只回 4 周 360 行(202636 / 202637 / 202638 / 202639),单独打 `week=202641` → **200 + total=0**;最新一期实测是两周前那一周。拿到空周先按"还没发布"理解,不是取数失败。⛔ `week_date` 是该周期的**截止交易日**,不是自然周边界:实测 202636→20260918(周五)、202637→20260924(周四)、202638→20260930(周三)、202639→20261009(周五,跨国庆),所以**不许按自然周对齐去补洞、也不许拿周码反推日期**,日期只认 `week_date` 这一列。单位:`volume_lots` / `cum_volume_lots` / `open_interest_lots` 是**手**;`amount_yi` / `cum_amount_yi` 是**亿元**——这条不是文档给的(文档只写"成交额"),是把 202639 那 90 行逐行拿 `amount × 1e8 ÷ vol` 与「`ftshare_futures_contract_detail` 里同品种的众数合约乘数 × `week_close_price`」相对照算出来的:82 行可比(另外 8 行要么当周无成交、要么 `prd` 是 `L-F` 这种清单里没有的写法),比值全部落在 **0.969~1.085**、中位 **0.999**(差额是周内加权均价与周末收盘价的差)。`*_yoy` / `open_interest_wow_pct` / `close_wow_pct` 是**百分数原值**(6.64 = 6.64%),不是比例值。⛔ 同比列的上游键名拼错:报文里写作 **`amout_yoy`**(少一个 n),而同一行的 `amount` 拼写正常,所以那是键名笔误不是另一种口径——本层改名成 `amount_yoy_pct` 透出(与东财 `SECUCODE → thscode` 同一做法)。无成交的那一行**不是 0 而是空洞**:实测 202639 里 JR / PM / RI / WH / ZC 五行 `volume_lots=0`、`cum_volume_lots=0`、`open_interest_lots=0`,而 `volume_yoy_pct` / `amount_yoy_pct` / `week_close_price` / `close_wow_pct` 全为 null(实测 90 行里 10 行 `volume_yoy_pct` 为 null、5 行 `week_close_price` 为 null)——null 保持 null,不补 0 也不沿用上期。';
|
|
2971
|
-
/**
|
|
2771
|
+
/** ⛔ Upstream misspells `amount_yoy` as `amout_yoy`; renamed here. */
|
|
2972
2772
|
function createFuturesWeeklySource(client) {
|
|
2973
2773
|
const capability = 'ftshare_futures_weekly';
|
|
2974
2774
|
const allowed = ['week', 'start_week', 'end_week', 'prd', 'exchange', 'page', 'size'];
|
|
2975
2775
|
const path = '/api/v1/market/data/futures/fut-weekly-detail';
|
|
2976
2776
|
const schema = {
|
|
2977
2777
|
capability,
|
|
2978
|
-
//
|
|
2778
|
+
// No time contract: parameters have only a week code; the row date is `week_date`.
|
|
2979
2779
|
name: 'get_futures_weekly_detail',
|
|
2980
2780
|
source: 'http:ftshare.fut_weekly_detail',
|
|
2981
2781
|
data_key: buildDataKey('ftshare', 'http', capability),
|
|
@@ -3037,8 +2837,7 @@ function createFuturesWeeklySource(client) {
|
|
|
3037
2837
|
if (params.week !== undefined)
|
|
3038
2838
|
assertFiltered(rows, { field: 'week', expected: String(params.week), label: 'week' });
|
|
3039
2839
|
else if (params.start_week !== undefined && params.end_week !== undefined) {
|
|
3040
|
-
//
|
|
3041
|
-
// 换成"那一年的 1/1..12/31"这种宽框等于不核。
|
|
2840
|
+
// `YYYYWW` sorts chronologically; compare against the requested week, not a whole year.
|
|
3042
2841
|
const wrong = rows.find((row) => String(row.week ?? '') < String(params.start_week) || String(row.week ?? '') > String(params.end_week));
|
|
3043
2842
|
if (wrong !== undefined)
|
|
3044
2843
|
throw new FtshareError('invalid_response', `FTShare returned week ${JSON.stringify(String(wrong.week ?? ''))} outside the requested ${params.start_week}..${params.end_week} range — 上游没有应用周区间,这一份不是该区间的结果`);
|
|
@@ -3046,7 +2845,7 @@ function createFuturesWeeklySource(client) {
|
|
|
3046
2845
|
if (params.exchange !== undefined)
|
|
3047
2846
|
assertFiltered(rows, { field: 'exchange', expected: String(params.exchange), label: 'exchange' });
|
|
3048
2847
|
if (params.prd !== undefined) {
|
|
3049
|
-
//
|
|
2848
|
+
// Compare the whole comma-joined product filter, not one value.
|
|
3050
2849
|
const wanted = String(params.prd).split(',').map((item) => item.trim()).filter((item) => item !== '');
|
|
3051
2850
|
const wrong = rows.find((row) => !wanted.includes(String(row.prd ?? '').toUpperCase()));
|
|
3052
2851
|
if (wrong !== undefined)
|
|
@@ -3088,7 +2887,7 @@ const nanhuaIndexRow = {
|
|
|
3088
2887
|
additionalProperties: false,
|
|
3089
2888
|
};
|
|
3090
2889
|
const NANHUA_DESCRIPTION = '按指数代码获取**南华期货指数日 K 线**(商品指数序列,代码形如 `AG_NH` / `CU_NH`)。两种查法:`trade_date` = 单日截面(实测 AG_NH + 20261015 回 1 行;⛔ 传了 `trade_date` 时区间参数被忽略,这是文档写的、实测单日只回 1 行与之相符),或 `start_date` + `end_date` = 区间序列(实测 AG_NH 20260817..20261015 回 total=38、按 `trade_date` **升序**)。`code` 与 `trade_date` 至少给一个(实测两者都缺 → 400「code and trade_date cannot both be empty」)。`size` 上限 **200**(实测 201 → **响亮** 400「page_size must be between 1 and 200」,这一颗与静默夹断那几颗不同);`page` 实测生效(page=2 / size=10 首行 20260831,与第一页零重叠)。⛔ **OHLC 不保证自洽**:指数是聚合出来的,实测 30 行里就有 **1 行 `high < open`**(AG_NH 20260928:open 1758.78 / high 1724.75 / low 1649.31 / close 1650.30)——不要拿单根 K 的高低自洽性判这份数据"坏了",更不要因此重试。可信的是涨跌那一组:实测 `change ≡ close − pre_close` 与 `pct_chg ≡ change ÷ pre_close × 100` 在 30 行里 **0 违例**。`change_pct` 是百分数原值(-4.3166 = 跌 4.32%)。⛔ `amount_yi` 的单位是**本层核出来的,文档那条是错的**:文档写「成交额(千元)」,而按千元读 CU_NH 20260930 那一行(`amount=802.72` / `vol=146,524` 手)得到每手只有 5.48 元、除以铜的 5 吨/手是 **1.1 元/吨**——比同日 `ftshare_futures_kline` 的 CU2612 收盘 108,290 元/吨低五个数量级;按**亿元**读才是每手 547,842 元、折合 **109,568 元/吨**,与 108,290 同量级(差 1.2%,指数覆盖多只合约)。所以这一列按亿元命名,但⛔ 它仍是**推断出的量级**:只做同 `code` 内的时序与相对比较,不要拿它和股票或单个合约的成交额对账。`volume_lots` 是手。认不出的代码不回错、只回空分页(实测 `ZZZZ_NH` → 200 + total=0)。文档口径覆盖自 20211115 起,本层未逐品种核起点。行内 `trade_date` 上游是**字符串** `YYYYMMDD`,本层归一成 ISO + 毫秒轴。';
|
|
3091
|
-
/**
|
|
2890
|
+
/** ⛔ Aggregate index does not guarantee high ≥ open. */
|
|
3092
2891
|
function createNanhuaIndexKlineSource(client) {
|
|
3093
2892
|
const capability = 'ftshare_futures_index_kline';
|
|
3094
2893
|
const allowed = ['code', 'trade_date', 'start_date', 'end_date', 'page', 'size'];
|
|
@@ -3206,7 +3005,7 @@ const memberRankRow = {
|
|
|
3206
3005
|
additionalProperties: false,
|
|
3207
3006
|
};
|
|
3208
3007
|
const MEMBER_RANK_DESCRIPTION = '按合约与会员维度获取**东方财富期货每日成交持仓排名**,一行是「一只合约 × 一家会员」。⛔ 这一族的写法坑本层已经替模型填平:实测上游 `exchange` **只认小写**、且广期所的真名是 `gfe`——`GFEX` 与 `gfex` 都回 **200 + 0 行、不报错**(实测同一个交易日 `gfe` 回 total=196、`gfex` 回 0),`variety_code` 同样大小写敏感(`AG` → 0 行、`ag` → total=100)。本层把六家常规写法换成上游那一套小写(GFEX → `gfe`)、品种转小写,`contract_code` 保持原样大写发(实测 `AG2610` 命中 20 行)。`trade_date` / `start_date` / `end_date` 都收(实测 `2026-10-15` 与 `20261015` 两种写法都回同一个 total=6911),行里的日期上游写作带横杠的 `2026-10-15`。⛔ **一定要带到合约级别的过滤**:实测不给任何日期 = 全表 1,435,879 行,给一个交易日 = 6,911 行,再加 `contract_code` = 20 行——只给日期的那次一次返回必然超闸(回 `too_large` + `profile_ref`,请转 `query_dataset` 收窄),什么都不给的那次翻页要两千多页。`size` 上限 **500**(实测 5000 → **响亮** 400「page_size must be between 1 and 500」)。⛔ **空串是这张表的常态而不是脏数据**:实测一个交易日 100 行里 `long_rank` / `long_position` 各 21 行为空串、空头那一组 25 行、净多与净空那一组 59~64 行;而且**排名列与它的数值列同时为空**(实测五对里 mismatch 0 对)——那读作"这一行没有这个口径",不是"排名第 0",本层一律转成 null、不补 0。身份:`org_code` 才是会员的稳定身份(实测广期所与中金所的中信期货同为 `10058975`),`member_name` 带后缀且不统一(中金所那行是「中信期货(代客)」、广期所那行是「中信期货」)⇒ 按名字跨所聚合会把同一家拆成两半。行里 `exchange` 与 `variety_code` 是**小写**,`contract_code` 的大小写随交易所(实测 cffex 给 `IC2610`、gfe 给 `lc2611`)⇒ ⛔ 别拿它直接 join `ftshare_futures_contracts`(那边是 `IC2610.CFE` 带后缀)。单位:`volume_lots` 是手(文档明写"成交量(手)"),持仓与变动那些列上游只对 volume 一列声明过单位,本层按同族惯例读作手但**不承诺换算**;`settle_price` / `long_avg_price` / `short_avg_price` 是合约报价单位。';
|
|
3209
|
-
/**
|
|
3008
|
+
/** ⛔ Exchange is lowercase and GFEX is written `gfe`. */
|
|
3210
3009
|
function createFuturesMemberRankSource(client) {
|
|
3211
3010
|
const capability = 'ftshare_futures_member_rank';
|
|
3212
3011
|
const allowed = ['exchange', 'variety_code', 'contract_code', 'member_name_abbr', 'trade_date', 'start_date', 'end_date', 'page', 'size'];
|
|
@@ -3278,7 +3077,7 @@ function createFuturesMemberRankSource(client) {
|
|
|
3278
3077
|
assertFiltered(rows, { field: 'trade_date', expected: String(params.trade_date), label: 'trade_date' });
|
|
3279
3078
|
if (params.start_date !== undefined && params.end_date !== undefined)
|
|
3280
3079
|
assertInRange(rows, String(params.start_date), String(params.end_date), (row) => row.trade_date);
|
|
3281
|
-
//
|
|
3080
|
+
// Compare the lowercased row column with the value actually sent.
|
|
3282
3081
|
if (params.exchange !== undefined)
|
|
3283
3082
|
assertFiltered(rows, { field: 'exchange', expected: String(params.exchange), label: 'exchange' });
|
|
3284
3083
|
if (params.variety_code !== undefined)
|
|
@@ -3317,7 +3116,7 @@ const bullionRow = {
|
|
|
3317
3116
|
additionalProperties: false,
|
|
3318
3117
|
};
|
|
3319
3118
|
const BULLION_DESCRIPTION = '按日期区间获取**贵金属行情**(一行一个「日期 + 读数」)。`symbol` / `start_date` / `end_date` / `page` / `page_size` 五样都必须给(实测少 `page` → 400「缺少必填参数:page」,这一条文档与实测一致)。`size` 上限 **1000**(实测 1001 → **静默**夹到 1000 并回显 `pageSize: 1000`,超限不报错,所以只能在参数层拦)。⛔ **只有两个代码实测有数**:`AU9999` total=635、`XAUUSD` total=676(窗口 2024-01-01..2026-10-15);`AG9999` / `XAGUSD` / `XAU` 都是 **200 + total=0、不报错**(文档自己也写"不支持的 symbol 不报错")——拿到空分页是合法的"这一族没有这个标的",不要换写法重试。⛔ **序列停在 2026 年 8 月中旬**:实测 AU9999 大窗首行(= 窗内最新一天)是 **2026-08-14**、XAUUSD 是 2026-08-12,而 2026-10-06..10-15 那一窗 total=0——按"最近一周"打永远是空,请从有数的区间取。返回按日期**降序**(实测第一页首行 2026-08-14、末行 2026-08-03),与 K 线那一族相反。⛔ **单位上游不披露,本层不承诺**:AU9999 末行读数 940.72、XAUUSD 末行 4408.32,两者差一个数量级(一个是国内金、一个是国际金价,量纲显然不同),所以只做**同 `symbol` 内**的时序与相对比较,禁止跨 symbol 比价、禁止换算成元/克或美元/盎司。`price_value` 上游以字符串给高精度数(本层转数值,空值转 null);`quote_date` 上游已是 `YYYY-MM-DD`,本层补一条毫秒轴。';
|
|
3320
|
-
/**
|
|
3119
|
+
/** ⛔ Unit undisclosed; series ends 2026-08. */
|
|
3321
3120
|
function createBullionPriceSource(client) {
|
|
3322
3121
|
const capability = 'ftshare_bullion_price';
|
|
3323
3122
|
const allowed = ['symbol', 'start_date', 'end_date', 'page', 'size'];
|
|
@@ -3456,8 +3255,7 @@ export function createFtshareSources(resolveApiKey) {
|
|
|
3456
3255
|
tenors: ['on_rate', 'w1', 'w2', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12'],
|
|
3457
3256
|
}),
|
|
3458
3257
|
createFinancialCalendarSource(client),
|
|
3459
|
-
//
|
|
3460
|
-
// 它一次回 12 个指标、三种载荷形状,落成行集需要 hub 支持"一行变多行",不属于这一批。
|
|
3258
|
+
// Futures and metals; eastmoney_futures_strange needs one-to-many row support in hub.
|
|
3461
3259
|
createFuturesContractsSource(client),
|
|
3462
3260
|
createFuturesContractDetailSource(client),
|
|
3463
3261
|
createFuturesKlineSource(client),
|
|
@@ -3470,12 +3268,7 @@ export function createFtshareSources(resolveApiKey) {
|
|
|
3470
3268
|
createBullionPriceSource(client),
|
|
3471
3269
|
];
|
|
3472
3270
|
}
|
|
3473
|
-
/**
|
|
3474
|
-
* 盘中主力资金五档(同花顺口径)。这一条与日度那族**不是一回事**,不要合并成一颗能力:
|
|
3475
|
-
* 它的行是「某只票 × 某个 15 分钟切片」,单位是**元**、数值上游用字符串给,
|
|
3476
|
-
* 而且实测能翻到约 187 个交易日(日度那族只有 45 天)。⛔ `page_size` 的真实上限是 **500**:
|
|
3477
|
-
* 实测传 1000/2000 都只回 500 行且不报错(`pageSize` 回显 500),所以参数层直接拒超限。
|
|
3478
|
-
*/
|
|
3271
|
+
/** Intraday 5-tier flow: stock × 15-min slice, yuan, ~187 days; ⛔ page_size caps at 500 (silent truncation). */
|
|
3479
3272
|
function createMainCapitalSource(client) {
|
|
3480
3273
|
const capability = 'ftshare_main_capital_flow';
|
|
3481
3274
|
const allowed = ['date', 'time', 'page', 'size'];
|
|
@@ -3504,7 +3297,6 @@ function createMainCapitalSource(client) {
|
|
|
3504
3297
|
output_schema: output(mainCapitalRow),
|
|
3505
3298
|
};
|
|
3506
3299
|
const normalize = (params) => {
|
|
3507
|
-
// ⛔ 上限押 500(不是那三族的 1000):实测这一条对 page_size=1000 静默截成 500 行,超限不报错。
|
|
3508
3300
|
const { page, size } = pageParams(params, allowed, MAX_PAGE_SIZE_500);
|
|
3509
3301
|
const normalized = { page, size };
|
|
3510
3302
|
if (params.date !== undefined)
|