gangtise-openapi-cli 0.43.0 → 0.43.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/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  README 仅列最近 5 个版本摘要:
8
8
 
9
+ - **v0.43.1 — 2026-09-27**:① **EDE `fiscalYear` 漏传不报错**:预测类(`frcst_*`)与分红类(`div_cash_yr` 等)漏传时按服务端自定的默认年度取数,数看着正常但未必是你要的年度——**一律显式传 `fiscalYear`**;其余必填参数缺失仍报 `100001`。② **翻页去重**:同一 ID 的任一已出现版本再次出现都按重复去掉(计入 `duplicateRows`),不区分字段顺序。③ **观点 `detail`**:某一批夹有空元素或格式异常的元素时,这一批已返回的正文照常输出;空元素的 ID 列在 `missingIds`,其余没取到的列在 `unfetchedIds`(`unfetchedError` 带 traceId)。④ 文档:`finc_roe_avg_avg` 示例补 `reportDate`;债券三个评级命令的计费说明统一。
10
+
9
11
  - **v0.43.0 — 2026-09-26**:① **按条计费列表的额度保护**:省略 `--size` 时先拿到 `total` 估算全量积分(整页超过 50 积分的列表只先取 1 条),超过 1000 积分报错退出 1,加 `--yes` 或传 `--size N` 放行。② **翻页结果的完整性**:跨页重复的行去掉并标 `duplicateRows`;同一 ID 在后面的页内容变了,两版都保留并标 `changedRows`;`total` 正好等于偏移窗口时标 `totalCapped`;都退出 3。热点话题、纪要、A 股 / 港股公告、财报日历查不到内容时按空结果、退出 0(此前退出 3)。③ **`quote` 的 `--field` 在多只证券或全市场时自动补身份列**(日 K 补 `securityCode` / `tradeDate`、分钟 K 补 `securityCode` / `tradeTime`、`realtime` 补 `securityCode`;单只不补),**多只证券只点一列的脚本输出会多出这些列**。④ **多证券日 K 合批**:按行数上限装入尽量多的证券,请求数大幅减少;全市场分片按工作日计(港股 2 个工作日一片),全市场关键字须同时给起止日期。⑤ **首行晚到提示**:行情首行比请求起点晚 14 天以上时 stderr 提示(上市较晚或越过回溯窗口)。⑥ **`ai stock-summary` 与 `fundamental earning-forecast` 超时 / 5xx 不再自动重试**(单次可能扣数千积分)。⑦ 四个枚举参数写错时本地报错;Ctrl-C / `kill` / 终端断开时清理暂存文件,进程以该信号结束(`$?` 为 130 / 143 / 129),脚本循环随之停下。⑧ Agent Skill 主文件精简,细节移到 `references/`。
10
12
 
11
13
  - **v0.42.0 — 2026-09-25**:① **数值参数只收十进制写法**:`--size 0x10`、`--size 1e3`、`--limit 5.0` 这类写法发请求前报错并点名参数(此前会被换算成 16 / 1000 / 5 照常发出);数字列表类参数逐项要求整数。**脚本里用了这类写法的,请改成普通整数。** ② **接管道时保留退出码**:`gangtise … | head` 提前关掉读端时,以已判定的退出码(`3` / `4`)结束,此前一律 `0`;脚本用 `set -o pipefail` 即可看到。③ **`GANGTISE_TIMEOUT_MS` 只收整数毫秒**:低于 1 秒回退默认、高于 1 小时按 1 小时,所写数值没有按原样生效时 stderr 提示一次。④ 观点 `detail` 与估值分析的返回结构与约定不符时报错并附 trace,不再按空结果处理。⑤ `fundamental valuation-analysis` 的 `--field` 不含数值列、接口返回 0 行时,stderr 说明原因。⑥ 性能:大批量导出按块写盘;自动翻页时单个慢页对其余页的拖累更小;`GANGTISE_PAGE_CONCURRENCY` 设到 16 以上时并发随之增加。
@@ -14,8 +16,6 @@ README 仅列最近 5 个版本摘要:
14
16
 
15
17
  - **v0.41.0 — 2026-09-24**:① **观点列表改走 v2,省积分**:`insight opinion list` / `foreign-opinion list` 默认只返回摘要(`brief`,外资另有 `briefTranslate`,取正文前 200 字),**1 积分/条**;要正文用新增的 `insight opinion detail --chief-opinion-id` / `foreign-opinion detail --foreign-opinion-id`(**30 积分/条**,按返回条数计;ID 可一次传多个,CLI 按 20 个一批自动拆分;没返回正文的 ID 列在 `missingIds`、中途某批失败时未取的 ID 列在 `unfetchedIds`,两种情况都以退出码 3 结束)。想像以前一样列表直接带正文,加 `--with-content`(30 积分/条,返回旧版结构:内资正文在 `contentList.content`;与 `detail` 一样超时不自动重发)。② **题材画像与成分股改走 v2**:`alternative concept-info` / `concept-securities` **50 积分/次**;v2 不含催化事件 `keyEvents`、重点个股标识 `isKey`、纳入理由 `inclusionReason`,需要这几列加 `--full`(500 积分/次)。③ **云盘管理 9 个命令,全部免费**:`vault drive-folder-list`(浏览目录)/ `drive-upload` / `drive-create-folder` / `drive-rename` / `drive-move-file` / `drive-move-folder` / `drive-copy`(把文件复制到另一空间:我的云盘 ↔ 租户云盘)/ `drive-delete-file` / `drive-delete-folder`。两个删除**需 `--yes`**,删文件夹会连同其中全部子文件夹与文件一起删除且不可恢复;云盘允许同名,上传 / 新建 / 复制每跑一次就多一份,这三个与两个删除**超时不自动重发**。④ **新增 `bond` 族 12 个命令,0.4 积分起**:`basic-info`(静态档案)/ `issuer-info` / `daily-quote` / `valuation`(上清所估值)/ `cash-flow` / `announcement` / `issuance-detail` / `rating-overview` / `rating-change`(债项评级变动)/ `issuer-rating-change` / `issuance-plan` / `exercise-notice`。多数按次计费;`rating-overview` 按条、`rating-change` 按有数据的债券只数、`issuer-rating-change` 按发行人计。`--security` **只收标准债券代码**(`019742.SH` / `220205.IB`),简称与拼音整批拒绝(`120001`),先用 `reference securities-search` 换代码;`--field` 写错字段名同样整批拒绝(`100003`),不会静默丢列。**整族超时不自动重发**——计费接口重发会重复扣费,偶发 5xx / 超时请自行重跑。🔴 **`bond announcement` 需手动翻页**:本族唯一分页的接口且**不返回 `total`**,`--page-no` 从 1 起逐页递增直到某页为空。⑤ **`insight highlight list` 会议线索**(会议核心要点信息流;🔴 **5 积分/条,务必带 `--size`**,省略会拉全量;`content` 是 HTML 片段,`--research-area` 不认申万码;按偏移量最多能翻到第 10000 条,超出部分请缩短时间范围分段取)。⑥ **`tool web-search` 联网搜索**(1 积分/次,零结果不扣):`--site` 定向站点、`--min-tier` 收信源等级、`--freshness` 收时效、`--include-content` 取正文(此时 `--size` ≤5)。⚠️ **排序是「信源等级 → 发布日期 → 相关性」,首条不等于最相关**;`publishTime` 判不出时为 `null`,`--freshness` 会把这批一并滤掉,做时点判断别用 `indexTime`。⑦ **常量接口新增 9 个分类**:`fundType` / `fundBondType` / `bondType` / `interestRateType` / `interestFrequency` / `absUnderlyingAssetType` / `ratingType` / `exchange` / `nationalEconomicIndustry`。⑧ **取数前注意几处取值**:`bond issuer-info` 的评级列混合境内外口径,做信用比较一并取 `ratingAgency`;评级值可能带 `sf` / `pi` 后缀;`bond daily-quote` / `valuation` / `issuance-plan` 的 `--start-date` 早于可回溯下界时整批返回 `110003`,把起点移进窗口再查。逐条见 `gangtise-openapi/references/commands/bond.md`。
16
18
 
17
- - **v0.40.1 — 2026-09-19**:**文档修正,无代码变更**。① **字段名写错时两族接口的表现完全不同,现在写明了**:`quote realtime` / `day-kline` / `minute-kline` / `fund-flow` 是**字段名和值一起消失**(列数与值数始终对得上,CLI 标 `partial` + `missingFields`、退出码 3);`fundamental main-business` / `valuation-analysis` 是**字段名照请求回显、行里少一个值**(`fieldList` 比行长,按下标对位会让缺口之后每个值都贴到错误的表头上,CLI 因此直接报错退出 1)。对照表在 `gangtise-openapi/references/fields.md` 顶部。② 订正 `references/commands/fundamental.md` 里一处把这两族说反的类比。**两族都不报错**,所以 `--field` 的字段名要以 `fields.md` 为准,不确定就别传(不传即返回全部字段)。
18
-
19
19
  ### 历史里程碑
20
20
 
21
21
  - **v0.40.0**:自选股股票池可增删改(首批写操作,删池须显式 `--yes`);`indicator time-series` 自动判定日期轴;Token 缓存绑定签发它的凭证。
@@ -288,7 +288,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
288
288
  - **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
289
289
  - **流式输出**:`--format jsonl` 或 `csv` 加 `--output <file>` 时,翻页 / 全市场分片 / 多证券分批请求的行**按到达顺序逐批写盘**,取数阶段不持有整份结果,内存不随行数增长;csv 先落临时行文件、收尾时按列并集写表头再转成 csv(磁盘两遍、内存不变)。不足 1000 行的结果一次整体写出。任一只 / 页 / 片失败,命令退出时后台已无取数与写盘,不留 `.part`。
290
290
  - **导出元信息**:`csv` / `jsonl` 落盘时旁边生成 `<文件>.meta.json`——命令行、数据行数、列名、`complete`(写元信息时的判定:退出 3 即 `false`)、`total` / `partial` / `failedPages` / `failedShards` / `truncatedShards` / `droppedColumns` / `missingFields` / `duplicateRows` / `changedRows` / `totalCapped` 等全部完整性标记、抓取时间与时区、CLI 版本,以及数据文件的字节数 `bytes` 与内容哈希 `sha256`。命令行与结果里的 key / secret / token 类字段写成 `[redacted]`。元信息在数据文件发布之后才就位,导出失败不会留下新数据配旧元信息。**转交或归档前建议核一次**:`shasum -a 256 <文件>` 与元信息里的 `sha256` 相等,才说明这份元信息描述的就是旁边这份数据——两个进程同时导出到**同一个 `--output`** 时,最终文件一定是其中某一次的完整产物,但旁边的元信息有可能来自另一次,核哈希就能发现。**输的那一次自己也会报**:收尾时回读一次 `--output`,与本次写出的哈希不符就在 stderr 说明「这个文件不是本次命令的产物」并**退出码 4**。这两种格式的文件本身只有数据行,转交之后靠它核验是否完整;`json` 自带标记,不生成。
291
- - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**不重放的端点例外**(贵档:one-pager 等生成/提交类 + `tool file-parse` 提交 + 50/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`);按次计费的 `bond` 全部 12 个命令与 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共 46 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;不计分的那几个是**重放会造成副作用或把成功报成失败**——股票池名不允许重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`;云盘允许同名,重发上传 / 新建 / 复制会多出一份;重发一个其实已经删掉的删除,回来的是「文件不存在」或 `130002`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;代码或参数名写错报 `100003`、缺必填参数报 `100001`)。**终态码 `999011`(凭证无效)/ `140002`(终态失败:异步生成失败、参数错误或业务处理失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,`140002` 立即重发得到的还是同一结果。
291
+ - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**不重放的端点例外**(贵档:one-pager 等生成/提交类 + `tool file-parse` 提交 + 50/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`);`bond` 全部 12 个命令(多数按次,三个评级命令按条 / 按债券 / 按发行人)与按次计费的 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共 46 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;不计分的那几个是**重放会造成副作用或把成功报成失败**——股票池名不允许重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`;云盘允许同名,重发上传 / 新建 / 复制会多出一份;重发一个其实已经删掉的删除,回来的是「文件不存在」或 `130002`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;代码或参数名写错报 `100003`、缺必填参数报 `100001`,漏传 `fiscalYear` 例外、不报错)。**终态码 `999011`(凭证无效)/ `140002`(终态失败:异步生成失败、参数错误或业务处理失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,`140002` 立即重发得到的还是同一结果。
292
292
 
293
293
  <!-- no-replay-endpoints
294
294
  上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
@@ -803,9 +803,12 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
803
803
  # 前复权以最新交易日为基准,在最新交易日等于不复权、每次除权都会变,验证复权要用历史日
804
804
 
805
805
  # 必填参数:缺了会报 100001 并点名,按 parameterList 的 required 补齐再取:
806
- # N 期统计补 periodNum、年度/分红类补 fiscalYear;区间类要特定区间时传 sDate(可选,起始日,tradeDate 仍是终点)
806
+ # N 期统计补 periodNum、年度/分红/预测类补 fiscalYear;区间类要特定区间时传 sDate(可选,起始日,tradeDate 仍是终点)
807
+ # ⚠️ fiscalYear 漏传不报错:按一个服务端自定的默认年度取数(不随 --date 变),该年度有数就返回它、看着正常,没有就是 null,要显式传
808
+ # finc_roe_avg_avg 是报告期类,日期用 reportDate:
807
809
  gangtise indicator cross-section --indicator finc_roe_avg_avg --security 600519.SH \
808
- --date 2026-03-31 --indicator-param "finc_roe_avg_avg:periodNum=4"
810
+ --date 2026-03-31 --indicator-param "finc_roe_avg_avg:periodNum=4" \
811
+ --indicator-param "finc_roe_avg_avg:reportDate=2025-12-31"
809
812
 
810
813
  # 区间指标:sDate 是起点、--date 下发的 tradeDate 是终点,两者共存
811
814
  gangtise indicator cross-section --indicator qte_vol_intvl --security 600519.SH \
@@ -31,6 +31,15 @@ function markTotalCapped(out, why, collected, keepBelow) {
31
31
  out.partial = true;
32
32
  out.totalCapped = true;
33
33
  }
34
+ function stableStringify(value) {
35
+ if (Array.isArray(value))
36
+ return `[${value.map(stableStringify).join(",")}]`;
37
+ if (value && typeof value === "object") {
38
+ const obj = value;
39
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
40
+ }
41
+ return JSON.stringify(value);
42
+ }
34
43
  export class GangtiseClient {
35
44
  config;
36
45
  rowSink;
@@ -245,29 +254,33 @@ export class GangtiseClient {
245
254
  const dropRepeats = (rows) => {
246
255
  if (!seen || !rowId)
247
256
  return rows;
248
- const onThisPage = new Set();
257
+ const onThisPage = new Map();
249
258
  return rows.filter((row) => {
250
259
  const id = row && typeof row === "object" ? row[rowId] : undefined;
251
260
  if (id === undefined || id === null)
252
261
  return true;
253
- const digest = createHash("sha1").update(JSON.stringify(row)).digest("base64");
262
+ const digest = createHash("sha1").update(stableStringify(row)).digest("base64");
254
263
  const key = String(id);
255
- const previous = seen.get(key);
256
- const earlierOnThisPage = onThisPage.has(key);
257
- onThisPage.add(key);
258
- if (previous === undefined) {
259
- seen.set(key, digest);
264
+ const pageVersions = onThisPage.get(key);
265
+ if (pageVersions && !pageVersions.has(digest))
266
+ idIsRowKey = false;
267
+ if (pageVersions)
268
+ pageVersions.add(digest);
269
+ else
270
+ onThisPage.set(key, new Set([digest]));
271
+ const versions = seen.get(key);
272
+ if (versions === undefined) {
273
+ seen.set(key, new Set([digest]));
260
274
  return true;
261
275
  }
262
- if (previous !== digest) {
263
- if (earlierOnThisPage)
264
- idIsRowKey = false;
265
- else
266
- changedRows++;
267
- return true;
276
+ if (versions.has(digest)) {
277
+ duplicateRows++;
278
+ return false;
268
279
  }
269
- duplicateRows++;
270
- return false;
280
+ versions.add(digest);
281
+ if (!pageVersions)
282
+ changedRows++;
283
+ return true;
271
284
  });
272
285
  };
273
286
  const keep = async (fetched) => {
@@ -510,7 +523,7 @@ export class GangtiseClient {
510
523
  }).catch((error) => {
511
524
  const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
512
525
  if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
513
- throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名会报 100003 并指名)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——无数据返回保留行列的 null 单元格;code 或参数名写错报 100003、缺必填参数报 100001,都会指名。');
526
+ throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名会报 100003 并指名)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——无数据返回保留行列的 null 单元格;code 或参数名写错报 100003、缺必填参数报 100001,都会指名(fiscalYear 例外:漏传不报错,返回 null 或默认年度的值)。');
514
527
  }
515
528
  throw error;
516
529
  });
@@ -572,7 +572,7 @@ const ENDPOINT_DEFS = {
572
572
  path: "/application/open-fundamental/bond/rating-overview",
573
573
  kind: "json",
574
574
  description: "Query bond / issuer / guarantor ratings side by side (max 10 bonds per call)",
575
- billing: { per: "call", price: 0.4 },
575
+ billing: { per: "row", price: 0.4, maxUnits: 10 },
576
576
  retry: "no-replay",
577
577
  expects: "list",
578
578
  },
@@ -581,7 +581,7 @@ const ENDPOINT_DEFS = {
581
581
  path: "/application/open-fundamental/bond/rating-change",
582
582
  kind: "json",
583
583
  description: "Query bond rating change history (max 10 bonds per call)",
584
- billing: { per: "call", price: 0.4 },
584
+ billing: { per: "security", price: 0.4, maxUnits: 10 },
585
585
  retry: "no-replay",
586
586
  expects: "list",
587
587
  },
@@ -590,7 +590,7 @@ const ENDPOINT_DEFS = {
590
590
  path: "/application/open-fundamental/bond/issuer-rating-change",
591
591
  kind: "json",
592
592
  description: "Query issuer rating change history (by bond code or issuer name)",
593
- billing: { per: "call", price: 0.4 },
593
+ billing: { per: "issuer", price: 0.4, maxUnits: 10 },
594
594
  retry: "no-replay",
595
595
  expects: "list",
596
596
  },
@@ -1,4 +1,4 @@
1
- import { ApiError, ValidationError } from "./errors.js";
1
+ import { ApiError, markStructural, ValidationError } from "./errors.js";
2
2
  export async function fetchOpinionDetails(client, endpointKey, idListField, idField, ids) {
3
3
  const BATCH = 20;
4
4
  const unique = [...new Set(ids)];
@@ -7,18 +7,28 @@ export async function fetchOpinionDetails(client, endpointKey, idListField, idFi
7
7
  const list = [];
8
8
  let unfetched = [];
9
9
  let unfetchedError;
10
+ let nulls = 0;
10
11
  for (let i = 0; i < unique.length; i += BATCH) {
11
12
  try {
12
13
  const rows = await client.call(endpointKey, { [idListField]: unique.slice(i, i + BATCH) });
13
- list.push(...rows);
14
+ const bodies = rows.filter((row) => row !== null && typeof row === "object" && !Array.isArray(row));
15
+ const odd = rows.filter((row) => row !== null && (typeof row !== "object" || Array.isArray(row))).length;
16
+ list.push(...bodies);
17
+ if (odd > 0) {
18
+ throw markStructural(new ApiError(`${endpointKey} returned ${odd} non-object element(s) in its body list — the response layout may have changed`, undefined, undefined, rows));
19
+ }
20
+ nulls += rows.length - bodies.length;
14
21
  }
15
22
  catch (error) {
16
- if (i === 0)
23
+ if (i === 0 && list.length === 0)
17
24
  throw error;
18
- unfetched = unique.slice(i);
25
+ const kept = new Set(list.map((row) => String(row[idField])));
26
+ unfetched = unique.slice(i).filter((id) => !kept.has(id));
19
27
  const message = error instanceof Error ? error.message : String(error);
20
28
  unfetchedError = { message, ...(error instanceof ApiError ? { code: error.code, traceId: error.traceId } : {}) };
21
- process.stderr.write(`[gangtise] warning: ${endpointKey} failed on batch ${i / BATCH + 1} (${message}); ${unfetched.length} ID(s) not fetched: ${unfetched.join(", ")}. The bodies already returned are kept below — re-run for the IDs in unfetchedIds only.\n`);
29
+ process.stderr.write(unfetched.length > 0
30
+ ? `[gangtise] warning: ${endpointKey} failed on batch ${i / BATCH + 1} (${message}); ${unfetched.length} ID(s) not fetched: ${unfetched.join(", ")}. The bodies already returned are kept below — re-run for the IDs in unfetchedIds only.\n`
31
+ : `[gangtise] warning: ${message} (batch ${i / BATCH + 1}); every ID in it came back with a body.\n`);
22
32
  break;
23
33
  }
24
34
  }
@@ -26,7 +36,7 @@ export async function fetchOpinionDetails(client, endpointKey, idListField, idFi
26
36
  const missing = unique.filter((id) => !returned.has(id) && !unfetched.includes(id));
27
37
  const out = { total: list.length, list };
28
38
  if (missing.length > 0) {
29
- process.stderr.write(`[gangtise] warning: no body returned for ${missing.length} ID(s): ${missing.join(", ")} — the server skips an ID it has no body for without an error (e.g. a wrong ID; a just-published opinion may also not have its body yet). Result marked partial (exit 3).\n`);
39
+ process.stderr.write(`[gangtise] warning: no body returned for ${missing.length} ID(s): ${missing.join(", ")}${nulls > 0 ? ` (${nulls} came back as a null element)` : ""} — the server gives no error for an ID it has no body for (e.g. a wrong ID; a just-published opinion may also not have its body yet). Result marked partial (exit 3).\n`);
30
40
  out.missingIds = missing;
31
41
  }
32
42
  if (unfetched.length > 0) {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gangtise-openapi
3
- version: "0.43.0"
3
+ version: "0.43.1"
4
4
  description: |-
5
5
  通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
6
6
 
@@ -271,7 +271,7 @@ vault.stock-pool.create
271
271
  - 单证券 → 优先 `fundamental` 专用命令(多数免费 / 低价);多证券批量取**已实现**指标 → `indicator`(EDE)一次拉取:同一日期 / 报告期横比用 `cross-section`,区间走势用 `time-series`(不能多指标 × 多证券同时)
272
272
  - 始终不走 EDE:盈利预测 / 一致预期 → `fundamental earning-forecast`(EDE 的 EPS 是已实现值,**不能冒充预测**);A 股估值历史分位 → `valuation-analysis`;行情与 K 线 → `quote`;行业 / 宏观指标(无证券维度)→ `alternative edb-*`。**例外:总市值只有 EDE 有**——`indicator cross-section --indicator qte_mkt_cptl`(单位「元」,用 `--scale` 缩放)
273
273
  - 取数前 `indicator search --format json` 核三项:`indicatorName` + `description` 语义对、`scopeList` 覆盖全部目标市场、`parameterList` 必填参数可满足;任一不符就回退专用接口,港 / 美股缺专用能力时如实说不支持。「某市场无数据」这类否定结论以当次 `scopeList` + 抽查一行为准
274
- - 日期参数**只看 `parameterList` 必填哪个**:`tradeDate` → `--date`,`reportDate` → `--indicator-param "code:reportDate=..."`(多数报告期类要后者,但 `_ttm` 整族要 `tradeDate`);两个都必填(如 `div_cash_yld`)就两个都用 `--indicator-param` 显式给,只给 `reportDate` 时 `--date` 不再注入、会报 `100001`。日频估值(`finc_pe_ttm` / `finc_pb_mrq`)用最新交易日,用报告期末日期会取到陈值;参数名同样以 `parameterList` 为准(复权是 `adjustType`)
274
+ - 日期参数**只看 `parameterList` 必填哪个**:`tradeDate` → `--date`,`reportDate` → `--indicator-param "code:reportDate=..."`(多数报告期类要后者,但 `_ttm` 整族要 `tradeDate`);两个都必填(如 `div_cash_yld`)就两个都用 `--indicator-param` 显式给,只给 `reportDate` 时 `--date` 不再注入、会报 `100001`。🔴 `parameterList` 要 `fiscalYear` 的(预测类 `frcst_*`、分红类 `div_cash_yr` 等)一律显式给——漏传**不报错**,按一个服务端自定的默认年度取数(不随 `--date` 变):该年度有数就返回它、看着完全正常,没有就是 `null`。日频估值(`finc_pe_ttm` / `finc_pb_mrq`)用最新交易日,用报告期末日期会取到陈值;参数名同样以 `parameterList` 为准(复权是 `adjustType`)
275
275
  - 结果**按 `security` 字段取值**,别按请求下标对位(证券会按代码升序重排);批量回填加 `--key-by code`
276
276
  - 估值历史:EDE 与 `valuation-analysis` 的财报口径切换时点与财报版本都不同,做分位 / 回测两边都拉交叉核
277
277
  - "业绩点评"双义消歧:**检索已有**的业绩点评:研报走 `insight research list --llm-tag earningsReview`(0.1/条;`--llm-tag` 只有 `opinion` / `research` / `foreign-report` 三个列表有),业绩会纪要走 `insight summary list --category earningsCall`;**AI 现生成**一份走 `ai earnings-review`(异步、50/次)。不确定问一句
@@ -275,22 +275,22 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
275
275
 
276
276
  | 错误码 | 实际含义 | 怎么办 |
277
277
  | :--- | :--- | :--- |
278
- | `100001` | **缺少必填参数**:如 `universe` 没传,或指标 `parameterList` 里 `required` 的参数没给(msg 会点名是哪个指标的哪个参数) | 补齐 `--indicator` / `--security`,或用 `--indicator-param` 补上点名的参数 |
278
+ | `100001` | **缺少必填参数**:如 `universe` 没传,或指标 `parameterList` 里 `required` 的参数没给(msg 会点名是哪个指标的哪个参数)。⚠️ 漏传 `fiscalYear` **不报这个码**,见下「必填参数」 | 补齐 `--indicator` / `--security`,或用 `--indicator-param` 补上点名的参数 |
279
279
  | `100003`@400 | 入参/表达式错误:`time-series` 传了「多指标 × 多证券」、`expression` 引用未声明变量、`indicatorParamList` 的 code 不在 `indicatorCodeList` 里 | 按 msg 改;多 × 多改用 `cross-section`。CLI 已在本地拦截「表达式引用未绑定变量」,不会白发一次请求 |
280
280
  | `140002`@500 | **终态参数错**:枚举越界(如「参数 adjustType 的值 99 不在有效范围内 [1,2,3,4]」)、表达式语法错误 | **不重试**(CLI 已把 140002 列为终态码)。读 `search --format json` 的 `parameterList` 改参数名/取值 |
281
- | `999999` | 系统故障。「无数据」不用此码,所以它基本只剩真故障。⚠️ 无数据是占位单元格(统一 `null`);代码或参数名写错报 `100003`、缺必填参数报 `100001`,都会点名 | CLI 对 indicator 端点**不重试此码**;确认参数无误仍报错,稍后再试或联系平台支持 |
281
+ | `999999` | 系统故障。「无数据」不用此码,所以它基本只剩真故障。⚠️ 无数据是占位单元格(统一 `null`);代码或参数名写错报 `100003`、缺必填参数报 `100001`,都会点名(漏传 `fiscalYear` 例外,不报错) | CLI 对 indicator 端点**不重试此码**;确认参数无误仍报错,稍后再试或联系平台支持 |
282
282
  | `110003` | **超出账号数据权限的时间范围**。窗口按**账号**配、不按接口配——`cross-section` / `time-series` / `screener` 同界,区间**跨过**下界也整批报这个码;`quote day-kline` 在同一条界上,但跨界时从下界起返回、不报错 | 把起点移进范围;整段区间都早于下界时缩短窗口无用,**换接口绕不过去**,要更长历史联系客户经理开通 |
283
283
  | `130001` | 数据未找到,或**该指标无权限**(内层信封失败会带具体 msg,如"指标无权限";此码被服务端复用) | 检查查询条件与指标权限;换证券/日期仍失败多为无权限,联系管理员开通 |
284
284
 
285
285
  ### 指标专属参数
286
286
 
287
- 缺必填参数报 `100001` 并点名是哪个指标的哪个参数。**先完成语义 + `scopeList` + `parameterList` 三项校验;其中凡 `required:true` 的参数都用 `--indicator-param "指标code:参数=值"` 补上。** 常见的几个:
287
+ 缺必填参数报 `100001` 并点名是哪个指标的哪个参数。🔴 **`fiscalYear` 例外:漏传不报错**——预测类(如 `frcst_pe`)与分红类(`div_cash_yr` / `div_cash_paid_ratio`)都会按一个服务端自定的默认年度取数(不随 `--date` 变):该年度有数就返回它,看着完全正常,但未必是你要的那一年;没有就是 `null`,与「没有数据」无法区分。所以 `parameterList` 里标 `required` 的 `fiscalYear` 一律显式传。**先完成语义 + `scopeList` + `parameterList` 三项校验;其中凡 `required:true` 的参数都用 `--indicator-param "指标code:参数=值"` 补上。** 常见的几个:
288
288
 
289
289
  | 参数 | 适用指标 | 示例 |
290
290
  | :--- | :--- | :--- |
291
- | `periodNum` | N 期统计(N 期均值/最值,如 `finc_roe_avg_avg` 平均ROE N期均值) | `--indicator-param "finc_roe_avg_avg:periodNum=4"`;部分还需配**年报日期**才出数(如 `finc_roe_avg_avg` 用季末日期为空、用年报日期有数) |
291
+ | `periodNum` | N 期统计(N 期均值/最值,如 `finc_roe_avg_avg` 平均ROE N期均值) | `--indicator-param "finc_roe_avg_avg:periodNum=4"`;`finc_roe_avg_avg` 属报告期类,还要补 `--indicator-param "finc_roe_avg_avg:reportDate=2025-12-31"`(只给 `--date` 报 `100003`) |
292
292
  | `sDate` | 区间类的**起始日**(如 `qte_vol_intvl` 区间成交量、`qte_avg_vol` 区间日均成交量),格式 `yyyy-MM-dd` | `--indicator-param "qte_vol_intvl:sDate=2024-01-02"`。⚠️ **`sDate` 不能替代 `tradeDate`**——它是区间起点,`tradeDate`(=区间终点)仍是 required,`--date` 会照常下发。区间起始日的参数名就是 `sDate`,写成 `startDate` 会报 `100003 不支持参数 startDate`。⚠️ **不传 `sDate` 不报错**——它是可选参数,缺了就按默认区间算,所以要的是特定区间就必须显式传。另:`qte_amp_mo`(月振幅)等周期变体只吃 `tradeDate`,没有起始日参数 |
293
- | `fiscalYear` | 年度/报告期类(如 `div_cash_yr` 年度现金分红) | `--indicator-param "div_cash_yr:fiscalYear=2025"` |
293
+ | `fiscalYear` | 年度/报告期类(如 `div_cash_yr` 年度现金分红)、预测类(如 `frcst_pe`,与 `tradeDate` 同时必填)。**漏传不报错**,见上 | `--indicator-param "div_cash_yr:fiscalYear=2025"` |
294
294
  | `industryType` + `industryLevel` | `scr_indu` 所属行业(两个都 required,缺任一报 `100001`) | `--indicator-param "scr_indu:industryType=1" --indicator-param "scr_indu:industryLevel=0"` |
295
295
 
296
296
  > `paramValue` 一律按**字符串**约定传(`periodNum=4` 内部即 `"4"`,CLI 已处理)。
@@ -384,7 +384,7 @@ gangtise indicator cross-section --indicator scr_indu_citic --indicator scr_indu
384
384
  - **混合日期语义要拆查询**:同时要“某报告期营收 / EPS”和“估值 PE / PB”时,按各自有效日期分别调用 `cross-section` 再按 `security` 合并(财务=报告期末、PE/PB=最新交易日);不要把不同日期语义的指标塞进同一个 `--date`
385
385
  - **探索性取数**:缺值会保留行列并给占位单元格(含 1×1 的最简形态),占位值统一为 `null`;code 写错会直接报 `100003`,不会伪装成缺值。看趋势用 `time-series` + 覆盖报告期的区间,但不能把缺值当成通过语义 / scope 校验。
386
386
  - **名称反查 code 要核对,别取首条**:存在同显示名的兄弟指标——单季 `cf_finc_exp_qtr` 与累计 `cf_finc_exp` 都叫「财务费用」,`bs_fmt`/`cf_fmt`/`is_fmt` 都叫「报表格式」。`search` 按名称模糊匹配,目标 code 高概率在 top1 但不绝对,要看 `indicatorCode` 确认。
387
- - **批量查询出错先读 msg**:缺必填参数报 `100001`、代码或参数名写错报 `100003`,msg 都会点名是哪个指标或证券,照 msg 改即可。
387
+ - **批量查询出错先读 msg**:缺必填参数报 `100001`、代码或参数名写错报 `100003`,msg 都会点名是哪个指标或证券,照 msg 改即可。漏传 `fiscalYear` 不报错,见「必填参数」。
388
388
  - **市值量纲**:`qte_mkt_cptl`(总市值)与 `shr_tot`(总股本)**A/港/美股均有数**;**默认返原始「元」**(大市值公司是 `1e12` 量级,即万亿级),别误当天文数字。用 `scale` 数字码缩放(`0`元 / `3`千 / `4`万 / `6`百万 / `8`亿 / `9`十亿——`scale=8` 即以亿元为单位)、`currency` 换币种(**大写** `DFT`本币 / `CNY` / `HKD` / `USD` …)。**跨证券比市值前先统一 `scale`+`currency`**。
389
389
  - **币种与汇率**:`DFT`(原始币种)按市场识别——A股=CNY、港股行情=HKD、美股=USD;汇率换算自洽(互逆且三角一致)。⚠️ 但**同一只港股,行情类的原始币种是 HKD、财务类可能是 CNY**(如泡泡玛特财报以人民币计),跨市场比财务数据时显式传 `--currency CNY` 别依赖 `DFT`。另:财务类指标的汇率按**报告期**折算、行情类按查询日折算,两者隐含汇率会有细微差异,属正常口径差别。
390
390
  - **EDE 财务指标的 `reportType`**:`enumList` 的 label 与实际取数**一致**,按 label 传即可(取值以 `enumList` 为准):
@@ -419,6 +419,6 @@ gangtise indicator cross-section --indicator scr_indu_citic --indicator scr_indu
419
419
 
420
420
  - **发现流程**:`indicator search --format json` → 核对 `indicatorName` + `description`、`scopeList`(含 `usageRestriction`)、`parameterList`(**参数名以此为准**)→ 三项都通过才用 `cross-section` / `time-series` / `screener`
421
421
  - **积分**:`search` 免费;`cross-section` / `time-series` / `screener` 按请求单元格数量计费,标价为每 100 单元格 A 股 0.05 / 港股 0.1 / 美股 0.2 积分,每次查询不足 100 单元格按 100 计
422
- - **空结果排查顺序**:代码、参数名写错与缺必填参数都会直接报错并点名(`100003` / `100001`),不会以空结果出现。拿到整批 `null` 或 `screener` 空集时按序排查:① 日期语义对不对(`tradeDate` vs `reportDate`——报告期类指标日期用错会整批返 `null`,看着像「没数据」)② 该指标覆不覆盖这批证券(`scopeList`,单查一行确认)③ 真的没有数据
422
+ - **空结果排查顺序**:代码、参数名写错与缺必填参数会直接报错并点名(`100003` / `100001`),不会以空结果出现——已知例外是漏传 `fiscalYear`(按默认年度取数,该年度无数据时返回 `null`)。拿到整批 `null` 或 `screener` 空集时按序排查:① 日期语义对不对(`tradeDate` vs `reportDate`——报告期类指标日期用错会整批返 `null`,看着像「没数据」;`parameterList` 要 `fiscalYear` 的传了没有)② 该指标覆不覆盖这批证券(`scopeList`,单查一行确认)③ 真的没有数据
423
423
  - **数据权限**:正式账号行情 / 财务 / 指标类可回溯的年限按服务等级而定,试用账号更短。这个时间窗口按**账号**配、不按接口配——三个 EDE 接口同界,区间跨过下界也整批返 `110003`(`quote day-kline` 在同一条界上,但跨界时从下界起返回、不报错),**换接口绕不过去**;整段区间都早于下界时缩短窗口无用,要更长历史联系客户经理开通
424
424
  - 所有格式(table/json/jsonl/csv/markdown)均可用;导出宽表给 Excel 直接用 `--format csv --output xxx.csv`
@@ -47,7 +47,7 @@ gangtise insight opinion detail --chief-opinion-id <id> [--chief-opinion-id <id>
47
47
  - 🔴 **列表只含摘要**:`brief` 是正文前 200 字的截断,**1 积分/条**。要正文用 `detail` 按 `chiefOpinionId` 取(`content`,**30 积分/条**,按返回条数计);或 `list --with-content` 让列表直接带正文(30 积分/条)。只需判断相关性、做筛选时看 `brief` 就够,别默认加 `--with-content`
48
48
  - `detail` 与 `--with-content` 都按返回条数计费,**超时 / 5xx 不自动重发**(重发可能对已交付的正文再计一次费),偶发失败自行重跑
49
49
  - ⚠️ **`--with-content` 返回的是旧版结构**:标题与正文在 `contentList.title` / `contentList.content`(`contentList` 是对象,不是数组),顶层没有 `title` / `brief`。按 `content` 字段名取会取不到;要统一成 `detail` 的结构就走 `list` + `detail`
50
- - `detail`:ID 可重复传或逗号分隔,CLI 去重后按 **20 个一批**自动拆分请求。**没有有效正文的 ID 不报错、直接跳过**(ID 写错时即如此;刚发布的观点也可能暂时取不到,稍后重取)。CLI 比对请求与返回,缺的 ID 列在 `missingIds`;某一批请求失败时,已取到的正文照常输出,没取的 ID 列在 `unfetchedIds`、原因在 `unfetchedError`,只需对这些 ID 重跑。两种情况都标 `partial`、**退出 3**
50
+ - `detail`:ID 可重复传或逗号分隔,CLI 去重后按 **20 个一批**自动拆分请求。**没有有效正文的 ID 不报错、直接跳过**(ID 写错时即如此;刚发布的观点也可能暂时取不到,稍后重取)。CLI 比对请求与返回,缺的 ID 列在 `missingIds`;某一批请求失败或返回异常时,已取到的正文(包括这一批里已返回的)照常输出,没取到的 ID 列在 `unfetchedIds`、原因在 `unfetchedError`,只需对这些 ID 重跑。两种情况都标 `partial`、**退出 3**
51
51
  - 返回字段(list):`chiefOpinionId` / `publishTime` / `title` / `brief` / `author{chiefId, chiefName, researchAreaList, brokerID, brokerName}` / `securityList[]` / `industryList[]` / `conceptList[]` / `llmTagList`;`detail` 另加 `content`。`author.chiefId` / `chiefName` 在机构点评类观点上为 `null`
52
52
 
53
53
  - `--llm-tag`:`strongRcmd` 强烈推荐 | `earningsReview` 业绩点评 | `topBroker` 头部券商 | `newFortune` 新财富团队
@@ -13,7 +13,7 @@
13
13
  | 错误码 | 含义 | CLI 行为 | Agent 是否介入 |
14
14
  |--------|------|---------|--------------|
15
15
  | `100003` | 参数值非法——**最宽的兜底码**:类型错、`limit` 越界都归这里。**msg 通常已指明字段**(如「请求体字段类型不匹配: size 期望类型 Integer」「limit 最小为 1,最大为 10000」),先读 msg 再猜 | — | 按 msg 指的字段改;msg 没指明才对照 `--help` 查枚举拼写,**不要重试同命令** |
16
- | `999999` | 系统错误。`indicator`(EDE)的「无数据」不用此码(无数据是保留行列的占位单元格 `null`,见 `commands/indicator.md`;代码或参数名写错报 `100003`、缺必填参数报 `100001`,都会点名),所以这个码基本只剩真故障 | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
16
+ | `999999` | 系统错误。`indicator`(EDE)的「无数据」不用此码(无数据是保留行列的占位单元格 `null`,见 `commands/indicator.md`;代码或参数名写错报 `100003`、缺必填参数报 `100001`,都会点名;例外是漏传 `fiscalYear` 不报错,见 `commands/indicator.md`「必填参数」),所以这个码基本只剩真故障 | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
17
17
  | `410110` | **异步生成中**(HTTP 400,旧码)。新码 `140001`,CLI 两码都认 | 轮询视为 pending | 继续等 |
18
18
  | `410111` | **异步生成失败**(HTTP 400,旧码)。新码 `140002`,CLI 两码都认 | 终态 | **不重试**,换参数 |
19
19
  | `130002` | 资源不存在——下载类的常见码:`reportId` 不存在 / 非数字多归此码。另有更具体的 `130003`(资源未生成 / 无附件);`--file-type` 写错由 CLI 在本地报错、不发请求 | — | 确认 ID 有效且本账号可见;换 `--file-type` 或换一篇验证 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.43.0",
3
+ "version": "0.43.1",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {