gangtise-openapi-cli 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -20
- package/dist/src/cli.js +151 -55
- package/dist/src/core/commandBodies.js +9 -7
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/quoteSharding.js +11 -11
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +36 -35
- package/gangtise-openapi/references/commands/ai.md +2 -3
- package/gangtise-openapi/references/commands/fundamental.md +3 -2
- package/gangtise-openapi/references/commands/indicator.md +49 -33
- package/gangtise-openapi/references/commands/insight.md +10 -11
- package/gangtise-openapi/references/commands/quote.md +44 -25
- package/gangtise-openapi/references/commands/reference-and-lookup.md +3 -3
- package/gangtise-openapi/references/commands/vault.md +2 -1
- package/gangtise-openapi/references/examples.md +7 -7
- package/gangtise-openapi/references/fields.md +2 -2
- package/gangtise-openapi/references/lookup-ids.md +2 -2
- package/gangtise-openapi/references/response-schema.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
README 仅列最近 5 个版本摘要:
|
|
8
8
|
|
|
9
|
+
- **v0.34.0 — 2026-08-15**:跟进 2026-08-14 服务端更新。🔴 **破坏性**:`quote day-kline --security all` 已失效(服务端停止支持),改用 `aShares` / `hkStocks` / `usStocks`,且市场关键字必须单独传(不能与证券代码或另一个关键字混填)——这两种写法 CLI 都会在发请求前报错并指出正确写法;`ai stock-summary` 同理不再接受市场关键字。`day-kline` 现覆盖 A股/港股/美股个股与交易所/概念/行业指数(可混查),三个旧命令 `day-kline-hk`/`day-kline-us`/`index-day-kline` 标记为已下线;`minute-kline` 支持指数。**另两处影响取数完整性的修复**:`quote fund-flow` 把市场关键字与证券代码混填时,此前会只返回那几个代码的数据且不报错("全市场 + 这只"静默变成"只有这只"),现改为本地拦截;`quote index-day-kline --security all` 跨 30 天以上时分片过宽会撞行数上限被截断(标 `partial` + 退出 3),现已调细粒度,同区间可完整取回。另:三大报表新增 `earliestAnncDate`(做时点对齐用它,不要用 `announcementDate`),EDE 报告期类指标改为必须显式传 `reportDate`,错误码提示按新行为更新。
|
|
9
10
|
- **v0.33.0 — 2026-08-09**:四处行为变更,都是把「静默给出看着正常的错结果」改成显式失败——分页端点返回异形首包(含 `data: null`)改退出码 3;`total` 被服务端封顶时探测并标 `totalCapped` + 退出 3(三个 opinion 端点的 `total` 恒为 10000 而实际远不止,全量导出此前会被静默截断);空结果不再在 stdout 留空行、`null` 不再被渲染成一条记录。另补多处帮助文案与 EDE 占位值(个别指标填 `0` 而非 `null`)的说明。
|
|
10
|
-
- **v0.32.0 — 2026-08-08**:新增帕米尔专家纪要列表与下载;跟进 2026-08-07
|
|
11
|
+
- **v0.32.0 — 2026-08-08**:新增帕米尔专家纪要列表与下载;跟进 2026-08-07 服务端更新——EDE 缺数据不再整列/整行消失(退出码 3 现在专指「代码没被识别」),条件选股重复指标的 `unreliable` 告警随之移除;`--search-type`/`--rank-type`/`--file-type` 等枚举改为本地拦截(此前传非法值不会报错,会拿到未过滤的全量)。
|
|
11
12
|
- **v0.31.0 — 2026-08-03**:修复 v0.30.1 的矩阵维度校验误杀「单指标 × 板块 ID」时序查询的回归;EDE 结果不完整或不可信时改以退出码 3 标记(`partial` / `unreliable`)。
|
|
12
13
|
- **v0.30.1 — 2026-08-02**:修复 `sDate` 吞掉查询日期导致的区间指标静默错数,让条件选股的文本筛选真正可用,并在服务端整行/整列丢数据时给出警告。
|
|
13
14
|
- **v0.30.0 — 2026-08-02**:适配 EDE 接口重构(`universe` 取代 `securityCodeList`、截面矩阵转置、指标元数据结构化),新增 `indicator screener` 条件选股,并修正复权参数名。
|
|
@@ -170,9 +171,10 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
170
171
|
| | `concept-search` | 题材 ID 搜索(名称/拼音/分组名匹配) |
|
|
171
172
|
| | `sector-search` | 板块 ID 搜索(返回层级路径) |
|
|
172
173
|
| | `sector-constituents` | 板块成分股查询 |
|
|
173
|
-
| **Quote** | `day-kline`
|
|
174
|
-
| | `
|
|
175
|
-
| | `
|
|
174
|
+
| **Quote** | `day-kline` | 历史日K线——A股/港股/美股个股 + 交易所/概念/行业指数,可混查 |
|
|
175
|
+
| | `day-kline-hk` / `day-kline-us` | ⚠️ 已下线,能力并入 `day-kline`(接口仍可调,但不校验证券代码) |
|
|
176
|
+
| | `index-day-kline` | ⚠️ 已下线,能力并入 `day-kline`;但仍是取「全部沪深京指数」(`--security all`)和拿指数名称(`securityName`)的唯一方式 |
|
|
177
|
+
| | `minute-kline` | 分钟K线——沪深A股 + 各类指数(一次一只) |
|
|
176
178
|
| | `realtime` | 个股实时行情快照(A股/港股/美股) |
|
|
177
179
|
| | `fund-flow` | A股个股日资金流向(沪深京;小/中/大/特大单 + 主力净流入) |
|
|
178
180
|
| **Fundamental** | `income-statement` / `balance-sheet` / `cash-flow` | A股三大财务报表(累计) |
|
|
@@ -186,7 +188,7 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
186
188
|
| **AI** | `knowledge-batch` | 知识库批量检索 |
|
|
187
189
|
| | `knowledge-resource-download` | 知识资源下载 |
|
|
188
190
|
| | `security-clue` | 个股线索 |
|
|
189
|
-
| | `stock-summary` |
|
|
191
|
+
| | `stock-summary` | 个股看点(精炼投研总结,按代码批量、单次最多 6000 个;仅 A 股/港股,不支持全市场) |
|
|
190
192
|
| | `one-pager` | 一页通 |
|
|
191
193
|
| | `investment-logic` | 投资逻辑 |
|
|
192
194
|
| | `peer-comparison` | 同业对比 |
|
|
@@ -261,7 +263,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
261
263
|
- **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
|
|
262
264
|
- **自动重试**: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 下载,共 18 个):5xx/超时不重放——按次计费不幂等,重放即重复扣分;仅连接失败、429 与 token 自愈重试。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(该码 2026-08-01 前还兼表「查询无数据」,现在无数据是保留行列的占位单元格(多数指标 `null`、个别如 `is_dnrpnp` 是 `0`),空表另表示整轴 code 未识别)。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
|
|
263
265
|
- **Token 自愈**:调用返回 `0000001008` / `999002` 时自动强制刷新 Token 并重试一次。
|
|
264
|
-
- **K线/资金流向自动分片**:`quote day-kline --security
|
|
266
|
+
- **K线/资金流向自动分片**:`quote day-kline --security aShares|hkStocks|usStocks`、`quote fund-flow --security aShares` 等全市场查询自动按日期切分(A股 K线/资金流向 1 天/片、美股 1 天/片、港股 2 天/片;已下线的 `day-kline-hk`/`day-kline-us`/`index-day-kline` 用 `all`,分别 2/1/15 天/片),并发执行后合并结果;按日分片自动跳过周六日。分片时如果用户未传 `--limit`,自动注入 `limit: 10000`(API 上限)避免默认 6000 截断。
|
|
265
267
|
- **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
|
|
266
268
|
- **`--verbose`**:打印每个请求的方法、路径、状态码、耗时和响应大小到 stderr,方便定位慢查询。
|
|
267
269
|
|
|
@@ -300,7 +302,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
300
302
|
- `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
|
|
301
303
|
- 安全上限:自动翻页最多 1000 页,防止异常循环
|
|
302
304
|
- 部分页失败、或服务端实际返回行数与 `total` 矛盾(提前短页)时,不丢弃已取到的数据:结果带 `partial: true`(页失败时另有 `failedPages`;K线分片为 `failedShards`;`--format json` 可见),stderr 输出警告,**进程退出码为 3**(完整成功为 0)
|
|
303
|
-
- **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。**
|
|
305
|
+
- **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。**2026-08-15 起这个分支基本收不到样本**——服务端现在对解析不了的代码直接报 `100003` 并点名是哪个(指标码拼错 →「指标 xxx 不存在」;证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O` →「xxx 不是有效证券或者板块ID」),**无论同批有没有正确的代码都会报**,CLI 相应退出 1。真实的无数据/无覆盖仍是占位单元格 + 退出码 0。🔴 **占位值不统一**:多数指标是 `null`,个别(如 `is_dnrpnp`)是 `0`,而 `0` 会穿过比较与聚合——别把它当真值,详见 skill 的 `references/commands/indicator.md`。**条件选股的缺列另有更严的一档**:把缺列的变量当作无法求值,若表达式(按 `&&`/`||` 的布尔结构)再无任何可成立的分支,则**退出码 1 且不输出**——那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过。⚠️ 这一档以「服务端返回了命中行」为前提;**零命中时一律退出码 0**(没有行需要被质疑),所以空集不能直接当成「无标的符合条件」——头号嫌疑是某个变量拿到的是占位 `0`。语义约定:`0` 完整成功(含合法空结果)/`3` 有数据但不完整/`1` 硬失败
|
|
304
306
|
- **分页端点返回 `null` 也退出 3**:分页端点本该返回 `{total, list}`,真实的空结果是 `{total: 0, list: []}`。若响应体是 `null`(已知一例:`insight foreign-opinion` / `independent-opinion` 传 `--industry`),CLI 在 stderr 告警并**退出码 3**——只给告警的话,脚本无法区分「这个筛选确实没命中」和「这个筛选没生效」。机器格式(jsonl/csv)此时 **stdout 不输出任何字节**(不是空行),`--format json` 仍忠实打印 `null`。⚠️ 带 `--output` 时文件仍会被创建:csv 会写入 3 字节 UTF-8 BOM(Excel 兼容用),jsonl 为 0 字节——**按文件大小判空的脚本要留意 csv 的这 3 个字节**。
|
|
305
307
|
- 🔴 **`total` 被服务端封顶时会标 `totalCapped` 并退出 3**:`insight opinion` / `foreign-opinion` / `independent-opinion` 三个端点的 `total` 恒为 `10000`,而实际记录远不止(把 `from` 加到远超该值仍能取到真实记录)。省略 `--size` 的全量拉取本来会**正好取满 10000 条就停、且不报任何异常**——导出的文件是截断的却看不出来。现在全量拉取结束后会**多探一行**(`from = total`):探到数据就标 `partial` + `totalCapped` 并退出 3。判据不写死 10000,服务端改配置仍然有效;`total` 诚实时探针返回空、不产生计费。传了 `--size` 的有界请求不做此探测。
|
|
306
308
|
- 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`);其他格式下 stderr 输出 `Total: N, showing: M`(json 格式不输出该行)
|
|
@@ -436,16 +438,15 @@ gangtise reference sector-constituents --sector-id 1000001005 --format json
|
|
|
436
438
|
gangtise quote day-kline --security 600519.SH --start-date 2026-03-01 --end-date 2026-03-31
|
|
437
439
|
# 查最近/最新 K 线建议显式传 --start-date/--end-date;只传 --limit 会截取查询窗口开头,不等于最近N条
|
|
438
440
|
gangtise quote day-kline --format json
|
|
439
|
-
#
|
|
440
|
-
gangtise quote day-kline --security
|
|
441
|
-
#
|
|
442
|
-
gangtise quote day-kline
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
gangtise quote day-kline-us --security all --start-date 2026-04-01 --end-date 2026-04-02 --field securityCode --field close --format json
|
|
441
|
+
# 全市场查询:关键字是 aShares / hkStocks / usStocks,必须单独传(旧的 --security all 已不再支持)
|
|
442
|
+
gangtise quote day-kline --security aShares --start-date 2026-04-01 --end-date 2026-04-01 --limit 100 --format json
|
|
443
|
+
# 港股 / 美股 / 指数都走同一个 day-kline,可混着传
|
|
444
|
+
gangtise quote day-kline --security 00700.HK --security AAPL.O --start-date 2026-03-01 --end-date 2026-03-31
|
|
445
|
+
gangtise quote day-kline --security 000001.SH --security 880134.GT --security 821031.SWI --start-date 2026-03-01 --end-date 2026-03-31
|
|
446
|
+
# 港股全市场(自动按 2 天/片分片)
|
|
447
|
+
gangtise quote day-kline --security hkStocks --start-date 2026-04-01 --end-date 2026-04-10 --format json
|
|
448
|
+
# 美股全市场(自动按 1 天/片分片)
|
|
449
|
+
gangtise quote day-kline --security usStocks --start-date 2026-04-01 --end-date 2026-04-02 --field securityCode --field close --format json
|
|
449
450
|
# 沪深京指数日K线
|
|
450
451
|
gangtise quote index-day-kline --security 000001.SH --security 399001.SZ --start-date 2024-05-01 --end-date 2024-05-20 --field securityCode --field tradeDate --field close --field volume
|
|
451
452
|
# A股分钟K线
|
|
@@ -607,7 +608,7 @@ gangtise indicator screener --indicator F1:pty_op_scope \
|
|
|
607
608
|
--date 2026-07-31 --format table
|
|
608
609
|
|
|
609
610
|
# 复权 / 指标专属参数用 --indicator-param "code:key=value"
|
|
610
|
-
# ⚠️ 参数名必须以 search 的 parameterList
|
|
611
|
+
# ⚠️ 参数名必须以 search 的 parameterList 为准;写错名会报 100003 并指出是哪个指标的哪个参数
|
|
611
612
|
gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
612
613
|
--date 2024-01-02 --indicator-param "qte_close:adjustType=3" # 1不复权/2前复权/3后复权/4定点
|
|
613
614
|
# 不复权 1685.01 → 前复权 1531.225 → 后复权 13609.6168(前复权在最新交易日等于不复权,验证要用历史日)
|
|
@@ -725,14 +726,14 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
|
|
|
725
726
|
| `120001` | 证券代码无效(用 `reference securities-search` 确认代码与后缀) |
|
|
726
727
|
| `130001` | 数据未找到或无指标权限——**取代旧 `410004`** |
|
|
727
728
|
| `130002` | 资源不存在——**下载类的兜底码**,`--report-id` 不存在 / 非数字 / `--file-type` 非法都归这里(取代旧 `430004`) |
|
|
728
|
-
| `410110` / `410111` | 异步任务生成中(继续轮询)/
|
|
729
|
+
| `410110` / `410111` | 异步任务生成中(继续轮询)/ 生成失败(终态)——异步端点当前返回的是这两个码;新码为 `140001`/`140002`,CLI 两代都认 |
|
|
729
730
|
| `240001` | 财报期未披露或超出查询期(`earnings-review` 提交阶段即报,不扣积分) |
|
|
730
731
|
| `250001` | 不支持该数据源(`knowledge-resource-download` 需正确的 `resourceType + sourceId` 组合)——**取代旧 `433007`** |
|
|
731
732
|
| `900002` | 请求方法不正确(服务端 msg 为「请求类型有误」,HTTP 405) |
|
|
732
733
|
|
|
733
734
|
> **关于这次错误码重排**:服务端 2026-07-17 重排了 41 个公开码(三层:`999xxx` 服务统一层 / `1xxxxx` 业务通用层 / `2xxxxx` 接口专有层),信封新增 `errorType` 和 `traceId`。2026-07-20 逐码实测发现**迁移是按「错误处理层」而非按业务模块进行的**:同一个接口内,参数校验层与路由层已发新码,方法路由层、token 过滤器、以及异步生成状态仍发旧码。新码信封 `code` 是 JSON 数字且带 `errorType`,旧码是字符串且没有——但这判断的是单条错误路径,不是整个接口;CLI 对两代都能识别。报错行会带 `[trace <id>]`,**报障时请带上它**。
|
|
734
735
|
>
|
|
735
|
-
> 其余码(`999003`–`999006`、`999012`–`999016`、`100002`、`
|
|
736
|
+
> 其余码(`999003`–`999006`、`999012`–`999016`、`100002`、`210001`、`220001`、`230001`、`240002`、`240003`)在实测中未触发到,多被上面的兜底码接管,CLI 仍内置了对应提示。⚠️ 两个需要留意的行为:**枚举值拼错和分页越界在部分端点上会报 `100005`/`100006`、在另一些端点上被静默忽略**(后者按未传该筛选条件处理,结果看着正常但范围不对——v0.32.0 起 CLI 对 `--search-type`/`--rank-type`/`--file-type` 等已知枚举本地拦截,未覆盖的自由字符串参数要自己核对);**`viewpoint-debate` 的敏感内容不会被提前拦截**,会扣满 50 积分再以 `410111` 失败。
|
|
736
737
|
|
|
737
738
|
---
|
|
738
739
|
|
package/dist/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readTokenCache, redactTokenCache } from "./core/auth.js";
|
|
|
5
5
|
import { collectKeyValue, collectList, collectNumberList, dateArg, datetimeArg, screenerExpressionFields, isVersionNewer, localDateString, maybeArray, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseScreenerIndicators, parseSize, parseTimestamp13 } from "./core/args.js";
|
|
6
6
|
import { buildIndicatorCrossSectionBody, buildIndicatorScreenerBody, buildIndicatorTimeSeriesBody, buildQuoteKlineBody, buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
|
|
7
7
|
import { checkScreenerBindings, droppedFromMatrix, flattenCrossSection, flattenTimeSeries, isEmptyMatrix, requireIndicatorMatrix, unwrapIndicatorData } from "./core/indicatorMatrix.js";
|
|
8
|
-
import { callKlineWithSharding,
|
|
8
|
+
import { callKlineWithSharding, isFullMarket } from "./core/quoteSharding.js";
|
|
9
9
|
import { loadConfig } from "./core/config.js";
|
|
10
10
|
import { resolveTitle, saveDownloadResult, uniquePath } from "./core/download.js";
|
|
11
11
|
import { ENDPOINTS, listEndpoints } from "./core/endpoints.js";
|
|
@@ -441,7 +441,70 @@ insight.addCommand(qa);
|
|
|
441
441
|
insight.addCommand(reportImage);
|
|
442
442
|
program.addCommand(insight);
|
|
443
443
|
const quote = new Command("quote").description("Quote APIs");
|
|
444
|
-
const
|
|
444
|
+
const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
|
|
445
|
+
const LEGACY_ALL_MARKET = (shardDays) => ({ all: shardDays });
|
|
446
|
+
/** Realtime takes the same keywords but returns one snapshot per security, so there is
|
|
447
|
+
* nothing to shard and it only needs the accepted-keyword list. */
|
|
448
|
+
const REALTIME_MARKETS = ["aShares", "hkStocks", "usStocks"];
|
|
449
|
+
/** fund-flow is A-share only, so `aShares` is its sole whole-market keyword. */
|
|
450
|
+
const FUND_FLOW_MARKETS = ["aShares"];
|
|
451
|
+
/** Every keyword the quote APIs have ever taken, including ones a given command no
|
|
452
|
+
* longer accepts. What an unrecognised keyword does depends on the endpoint, and BOTH
|
|
453
|
+
* outcomes are worth a local error: the unified `day-kline` / `realtime` / `fund-flow`
|
|
454
|
+
* answer `120001` "invalid security code" (which sends the user hunting for a typo in a
|
|
455
|
+
* code that is fine), while the menu-retired per-market endpoints answer `code=000000`
|
|
456
|
+
* with `total: 0` — a silent empty result indistinguishable from "no data".
|
|
457
|
+
*
|
|
458
|
+
* Compared lower-cased, and 🔴 **that folding is load-bearing, not tidiness** — the API's
|
|
459
|
+
* own case handling differs BY ENDPOINT (probed 2026-08-15, all six):
|
|
460
|
+
*
|
|
461
|
+
* folds case: day-kline (aShares/…), realtime, day-kline-hk/-us/index (all)
|
|
462
|
+
* case-SENSITIVE: fund-flow — only the literal `aShares` works; `ashares` / `ASHARES`
|
|
463
|
+
* come back as `120001 非有效A股`
|
|
464
|
+
*
|
|
465
|
+
* So on five endpoints canonicalising merely keeps our shard lookup in step with the
|
|
466
|
+
* server (drop it and a case variant degrades to an unsharded 6000-row request), but on
|
|
467
|
+
* `fund-flow` it is the ONLY reason `--security ashares` works at all. Removing
|
|
468
|
+
* `canonicalizeMarketKeywords` would turn that from a working query into a hard error —
|
|
469
|
+
* see the fund-flow case test, which exists to pin exactly that.
|
|
470
|
+
*
|
|
471
|
+
* ⚠️ `all` collides with a real ticker root (`ALL` is Allstate on the NYSE), so a bare
|
|
472
|
+
* `--security ALL` fetches the whole US market instead of that stock. That resolution
|
|
473
|
+
* happens on the SERVER (`ALL` / `All` / `all` are equivalent to it on all three retired
|
|
474
|
+
* endpoints), so matching case-sensitively here would not prevent it — it would only stop
|
|
475
|
+
* us from sharding a request the server treats as whole-market anyway, turning a complete
|
|
476
|
+
* result into a 6000-row truncation. The fix for that user is the suffixed `ALL.N`.
|
|
477
|
+
*
|
|
478
|
+
* Unknown keywords are deliberately NOT rejected — this is a known-keyword list, so a
|
|
479
|
+
* future server-side addition degrades to "unsharded" rather than being refused outright
|
|
480
|
+
* (fail-open, no enum drift). */
|
|
481
|
+
const MARKET_KEYWORDS = new Set(["all", "ashares", "hkstocks", "usstocks"]);
|
|
482
|
+
const matchesMarketKeyword = (value, keyword) => value.toLowerCase() === keyword.toLowerCase();
|
|
483
|
+
const checkMarketKeywords = (securities, accepted, command) => {
|
|
484
|
+
const used = securities.filter((s) => MARKET_KEYWORDS.has(s.toLowerCase()));
|
|
485
|
+
if (used.length === 0)
|
|
486
|
+
return;
|
|
487
|
+
// Report an unsupported keyword before the alone-ness rule: when both are wrong, the
|
|
488
|
+
// keyword itself is the thing the user has to change.
|
|
489
|
+
const unsupported = used.filter((k) => !accepted.some((a) => matchesMarketKeyword(k, a)));
|
|
490
|
+
if (unsupported.length > 0) {
|
|
491
|
+
throw new ValidationError(accepted.length > 0
|
|
492
|
+
? `${command}: '${unsupported[0]}' is not a whole-market keyword for this command — use ${accepted.join(" / ")}`
|
|
493
|
+
: `${command}: this command takes explicit security codes only — '${unsupported[0]}' and other whole-market keywords are not supported`);
|
|
494
|
+
}
|
|
495
|
+
// The API rejects a keyword sent alongside security codes or a second keyword, again
|
|
496
|
+
// as a bare 120001 that points at the codes rather than at the combination. On
|
|
497
|
+
// `fund-flow` it is worse than a rejection: the keyword is silently dropped and only
|
|
498
|
+
// the explicit codes come back, exit 0.
|
|
499
|
+
if (securities.length > 1) {
|
|
500
|
+
throw new ValidationError(`${command}: a market keyword must be passed alone, got '${securities.join(", ")}' — the API rejects it mixed with security codes or another keyword`);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
/** Fold a user-typed keyword back to the spelling the sharding lookup expects, so a case
|
|
504
|
+
* variant reaches the same code path as the canonical form. Non-keywords pass through
|
|
505
|
+
* untouched. */
|
|
506
|
+
const canonicalizeMarketKeywords = (securities, accepted) => securities.map((s) => accepted.find((a) => matchesMarketKeyword(s, a)) ?? s);
|
|
507
|
+
const addKlineCommand = (name, endpointKey, securityHelp, markets) => quote.command(name)
|
|
445
508
|
.option("--security <code>", securityHelp, collectList, [])
|
|
446
509
|
.option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
|
|
447
510
|
.option("--end-date <date>", "End date (default: latest)", dateArg("--end-date"))
|
|
@@ -449,63 +512,93 @@ const addKlineCommand = (name, endpointKey, securityHelp, shardDays) => quote.co
|
|
|
449
512
|
.option("--field <field>", "Field", collectList, [])
|
|
450
513
|
.option("--format <format>", "Output format", "table")
|
|
451
514
|
.option("--output <path>")
|
|
452
|
-
.action((options) =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
515
|
+
.action((options) => {
|
|
516
|
+
// Validate BEFORE withClient: createClient() logs in when no token is cached, so a
|
|
517
|
+
// check inside the callback would spend a request to then fail locally anyway.
|
|
518
|
+
checkMarketKeywords(options.security, Object.keys(markets), `quote ${name}`);
|
|
519
|
+
options.security = canonicalizeMarketKeywords(options.security, Object.keys(markets));
|
|
520
|
+
return withClient(async (client) => {
|
|
521
|
+
const format = parseOutputFormat(options.format);
|
|
522
|
+
const body = buildQuoteKlineBody(options);
|
|
523
|
+
// Each market shards at its own granularity, so resolve which keyword was asked
|
|
524
|
+
// for before picking shardDays — a whole-market HK pull tolerates 2-day windows
|
|
525
|
+
// where A-share and US pulls need one day each.
|
|
526
|
+
const keyword = Object.keys(markets).find((k) => isFullMarket(body, k));
|
|
527
|
+
if (keyword) {
|
|
528
|
+
// A whole-market query is date-sharded: callKlineWithSharding lifts the limit to
|
|
529
|
+
// the API max and owns completeness (partial / failedShards), so leave `limit`
|
|
530
|
+
// unset and skip the single-request truncation guard.
|
|
531
|
+
const data = await callKlineWithSharding(client, endpointKey, body, { shardDays: markets[keyword], fullMarketValue: keyword });
|
|
532
|
+
await printData(data, format, options.output);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Explicit securities go out as one request: pin the limit to the known default so
|
|
536
|
+
// the sent limit and the truncation cap are the same number by construction.
|
|
537
|
+
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
538
|
+
// (Nothing to shard: sharding only ever applied to whole-market queries, so this
|
|
539
|
+
// is exactly the passthrough callKlineWithSharding would have done.)
|
|
540
|
+
const data = await client.call(endpointKey, { ...body, limit });
|
|
541
|
+
flagIfLimitTruncated(data, limit, name);
|
|
460
542
|
await printData(data, format, options.output);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
addKlineCommand("day-kline", "quote.day-kline", "
|
|
471
|
-
|
|
472
|
-
addKlineCommand("day-kline-us", "quote.day-kline-us", "Security code (US stock: e.g. AAPL.O, or 'all' for full market)", 1);
|
|
473
|
-
addKlineCommand("index-day-kline", "quote.index-day-kline", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", 30);
|
|
474
|
-
quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time")).option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
addKlineCommand("day-kline", "quote.day-kline", "Security code — A-share .SH/.SZ/.BJ, HK .HK, US .O/.N/.A, exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI; or one market keyword: aShares / hkStocks / usStocks (auto-sharded by date, must be passed alone)", KLINE_MARKETS);
|
|
546
|
+
addKlineCommand("day-kline-hk", "quote.day-kline-hk", "[deprecated: use 'day-kline'] Security code (HK stock: .HK, or 'all' for full market)", LEGACY_ALL_MARKET(2));
|
|
547
|
+
addKlineCommand("day-kline-us", "quote.day-kline-us", "[deprecated: use 'day-kline'] Security code (US stock: e.g. AAPL.O, or 'all' for full market)", LEGACY_ALL_MARKET(1));
|
|
548
|
+
// 15 days, not the historical 30: ~531 index rows per trading day x ~11 trading days in a
|
|
549
|
+
// 15-day window is ~5.8K, while a 30-day window is ~11.7K — every shard silently maxed out
|
|
550
|
+
// at the 10K cap and lost ~11% of the range (it surfaced as exit 3 + truncatedShards, but
|
|
551
|
+
// the split was never sized to avoid it in the first place).
|
|
552
|
+
addKlineCommand("index-day-kline", "quote.index-day-kline", "[deprecated: use 'day-kline'] Index code (.SH/.SZ/.BJ, or 'all' for full market)", LEGACY_ALL_MARKET(15));
|
|
553
|
+
quote.command("minute-kline").option("--security <code>", "Security code — A-share .SH/.SZ (SH/SZ only), exchange index .SH/.SZ, concept index .GT, industry index .CI/.SWI; no whole-market keyword").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time")).option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
475
554
|
const format = parseOutputFormat(options.format);
|
|
476
555
|
const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }) ?? DEFAULT_QUOTE_LIMIT;
|
|
477
556
|
const data = await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit, fieldList: maybeArray(options.field) });
|
|
478
557
|
flagIfLimitTruncated(data, limit, "minute-kline");
|
|
479
558
|
await printData(data, format, options.output);
|
|
480
559
|
}));
|
|
481
|
-
quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US)").option("--security <code>", "Security code
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
560
|
+
quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US stocks and indices)").option("--security <code>", "Security code — stock .SH/.SZ/.BJ/.HK/.O/.N/.A, exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI; or one market keyword: aShares / hkStocks / usStocks (must be passed alone; indices have no whole-market keyword)", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => {
|
|
561
|
+
// Realtime takes the same market keywords as day-kline but never shards (one snapshot
|
|
562
|
+
// per security), so it only needs the "alone, and a keyword this API knows" check.
|
|
563
|
+
checkMarketKeywords(options.security, REALTIME_MARKETS, "quote realtime");
|
|
564
|
+
return emit(options, (client) => client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList: maybeArray(options.field) }));
|
|
565
|
+
});
|
|
566
|
+
quote.command("fund-flow").description("A-share daily fund flow (SH/SZ/BJ)").option("--security <code>", "Security code (e.g. 600519.SH / 872931.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, []).option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)", dateArg("--start-date")).option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)", dateArg("--end-date")).option("--limit <number>", "Max rows per request (default: 6000, max: 10000; single-security cap — aShares auto-shards by day)").option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => {
|
|
567
|
+
// fund-flow needs this guard MORE than the kline commands, not less: mixing the keyword
|
|
568
|
+
// with codes doesn't even fail here. The server silently drops `aShares` and answers
|
|
569
|
+
// with just the explicit codes — one row, exit 0, no warning — so "whole market plus
|
|
570
|
+
// this one" quietly becomes "only this one". Validate before withClient so no login
|
|
571
|
+
// request is spent on a query that fails locally.
|
|
572
|
+
checkMarketKeywords(options.security, FUND_FLOW_MARKETS, "quote fund-flow");
|
|
573
|
+
options.security = canonicalizeMarketKeywords(options.security, FUND_FLOW_MARKETS);
|
|
574
|
+
return withClient(async (client) => {
|
|
575
|
+
const format = parseOutputFormat(options.format);
|
|
576
|
+
const body = {
|
|
577
|
+
securityList: maybeArray(options.security),
|
|
578
|
+
startDate: options.startDate,
|
|
579
|
+
endDate: options.endDate,
|
|
580
|
+
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }),
|
|
581
|
+
fieldList: maybeArray(options.field),
|
|
582
|
+
};
|
|
583
|
+
if (isFullMarket(body, "aShares")) {
|
|
584
|
+
// Full-market fund-flow: the server errors (430012/430013) instead of truncating when
|
|
585
|
+
// a single request exceeds the row cap, so date-shard by day (~5.4k A-share rows/day,
|
|
586
|
+
// under the lifted API cap) and merge — same mechanism as `--security all` kline.
|
|
587
|
+
// Sharding needs an explicit range; without both dates it would fall back to one
|
|
588
|
+
// doomed full-market request, so require the range up front with a clear message.
|
|
589
|
+
if (!body.startDate || !body.endDate) {
|
|
590
|
+
throw new ValidationError("quote fund-flow --security aShares requires both --start-date and --end-date (the full market is fetched via per-day shards)");
|
|
591
|
+
}
|
|
592
|
+
const data = await callKlineWithSharding(client, "quote.fund-flow", body, { shardDays: 1, fullMarketValue: "aShares" });
|
|
593
|
+
await printData(data, format, options.output);
|
|
594
|
+
return;
|
|
499
595
|
}
|
|
500
|
-
const
|
|
596
|
+
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
597
|
+
const data = await client.call("quote.fund-flow", { ...body, limit });
|
|
598
|
+
flagIfLimitTruncated(data, limit, "fund-flow");
|
|
501
599
|
await printData(data, format, options.output);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
505
|
-
const data = await client.call("quote.fund-flow", { ...body, limit });
|
|
506
|
-
flagIfLimitTruncated(data, limit, "fund-flow");
|
|
507
|
-
await printData(data, format, options.output);
|
|
508
|
-
}));
|
|
600
|
+
});
|
|
601
|
+
});
|
|
509
602
|
program.addCommand(quote);
|
|
510
603
|
const fundamental = new Command("fundamental").description("Fundamental APIs");
|
|
511
604
|
const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) })));
|
|
@@ -643,11 +736,14 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
|
|
|
643
736
|
}
|
|
644
737
|
}));
|
|
645
738
|
ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId from viewpoint-debate").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
|
|
646
|
-
ai.command("stock-summary").description("Stock highlights: refined research summary per security (A-share / HK)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK),
|
|
739
|
+
ai.command("stock-summary").description("Stock highlights: refined research summary per security (A-share / HK)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK), up to 6000 per call; market keywords are NOT supported by this endpoint", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => {
|
|
647
740
|
// Guard against an empty --security: omitting it would send securityList:undefined,
|
|
648
741
|
// which the backend may treat as all-market (3 credits/row × thousands of rows).
|
|
649
742
|
if (!options.security.length)
|
|
650
|
-
throw new ValidationError("--security is required: pass
|
|
743
|
+
throw new ValidationError("--security is required: pass one or more security codes (A-share / HK), up to 6000 per call");
|
|
744
|
+
// The endpoint dropped whole-market batches on 2026-08-14 and now answers a market
|
|
745
|
+
// keyword with 120001 "invalid security code" — which reads as a typo in the code.
|
|
746
|
+
checkMarketKeywords(options.security, [], "ai stock-summary");
|
|
651
747
|
return emit(options, (client) => client.call("ai.stock-summary.list", { securityList: maybeArray(options.security) }));
|
|
652
748
|
});
|
|
653
749
|
const reference = new Command("reference").description("Reference data APIs");
|
|
@@ -690,7 +786,7 @@ vault.command("record-list").option("--from <number>", "Starting offset", "0").o
|
|
|
690
786
|
addDownloadCommand(vault, { endpointKey: "vault.record.download", name: "record-download", idOption: "--record-id", idField: "recordId", fallbackPrefix: "record", contentTypeDescription: "Content type: original/asr/summary", titleListEndpoint: "vault.record.list" });
|
|
691
787
|
vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--research-area <id>", "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category), sourceList: options.source.length ? options.source : undefined }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
|
|
692
788
|
addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "my-conference-download", idOption: "--conference-id", idField: "conferenceId", fallbackPrefix: "conference", contentTypeDescription: "Content type: asr/summary", titleListEndpoint: "vault.my-conference.list" });
|
|
693
|
-
vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID --
|
|
789
|
+
vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID -- citicIndustry codes (1008001xx) only; swIndustry codes and unknown values are ignored and silently return the unfiltered total", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
|
|
694
790
|
vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
|
|
695
791
|
vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.list", {})));
|
|
696
792
|
vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; omit (or 'all') for all pools", collectList).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", buildStockPoolStocksBody(options))));
|
|
@@ -784,7 +880,7 @@ indicator.command("search").requiredOption("--keyword <text>", "Search keyword,
|
|
|
784
880
|
});
|
|
785
881
|
await printData(unwrapIndicatorData(raw), format, options.output);
|
|
786
882
|
}));
|
|
787
|
-
indicator.command("cross-section").option("--indicator <code>", "Indicator code, e.g. qte_close (REQUIRED, repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, []).requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as each indicator's tradeDate
|
|
883
|
+
indicator.command("cross-section").option("--indicator <code>", "Indicator code, e.g. qte_close (REQUIRED, repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, []).requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as each indicator's tradeDate. Report-period indicators (is_*, financial statements) REJECT tradeDate and require --indicator-param 'code:reportDate=yyyy-MM-dd' instead — check parameterList in 'indicator search'", dateArg("--date")).option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)").option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)").option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'", collectList, []).addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode, unique & order-stable for batch code→value mapping").choices(["name", "code"]).default("name")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
788
884
|
const format = parseOutputFormat(options.format);
|
|
789
885
|
requireIndicatorScope(options.indicator, options.security);
|
|
790
886
|
const raw = await client.call("indicator.cross-section", buildIndicatorCrossSectionBody(options));
|
|
@@ -809,7 +905,7 @@ indicator.command("time-series").option("--indicator <code>", "Indicator code, e
|
|
|
809
905
|
flagDropped(rows, data, options.security, options.indicator);
|
|
810
906
|
await printData(rows, format, options.output);
|
|
811
907
|
}));
|
|
812
|
-
indicator.command("screener").description("Screen securities by an expression over indicator values (条件选股)").option("--indicator <spec>", "Bind a variable to an indicator, 'F1:code', e.g. F1:qte_mkt_cptl (REQUIRED, repeat)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, []).requiredOption("--expression <expr>", "Filter over the bound variables, e.g. 'F1 >= 800 && (F2 >= 20 && F2 <= 30)'; also supports contains/notcontains on string indicators").requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as every indicator's tradeDate unless it already has one — omitting it leaves date-bearing indicators unfiltered and silently yields an empty screen", dateArg("--date")).option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'", collectList, []).addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode").choices(["name", "code"]).default("name")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
908
|
+
indicator.command("screener").description("Screen securities by an expression over indicator values (条件选股)").option("--indicator <spec>", "Bind a variable to an indicator, 'F1:code', e.g. F1:qte_mkt_cptl (REQUIRED, repeat)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, []).requiredOption("--expression <expr>", "Filter over the bound variables, e.g. 'F1 >= 800 && (F2 >= 20 && F2 <= 30)'; also supports contains/notcontains on string indicators").requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as every indicator's tradeDate unless it already has one — omitting it leaves date-bearing indicators unfiltered and silently yields an empty screen. Report-period indicators (is_*) reject tradeDate: give them --indicator-param 'F1:reportDate=yyyy-MM-dd'", dateArg("--date")).option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'", collectList, []).addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode").choices(["name", "code"]).default("name")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
813
909
|
const format = parseOutputFormat(options.format);
|
|
814
910
|
requireIndicatorScope(options.indicator, options.security);
|
|
815
911
|
// Binding one indicator code to several variables (the same price on two
|
|
@@ -39,13 +39,15 @@ export function buildStockPoolStocksBody(options) {
|
|
|
39
39
|
* indicator's `tradeDate`. A caller-supplied `reportDate` suppresses that
|
|
40
40
|
* injection for its indicator, so the two date fields are never sent together.
|
|
41
41
|
*
|
|
42
|
-
* This
|
|
43
|
-
* answered a `tradeDate` with an EMPTY result
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
42
|
+
* This is load-bearing, and the server has moved twice on it. A report-period
|
|
43
|
+
* indicator answered a `tradeDate` with an EMPTY result (probed 2026-08-01 on
|
|
44
|
+
* `is_op_rev_mom`), then resolved it to the enclosing report period (2026-08-08,
|
|
45
|
+
* same 33.4903 either way), and since 2026-08-14 REJECTS it outright:
|
|
46
|
+
* `100003 指标 is_op_rev_mom 不支持参数 tradeDate; 缺少必填参数 reportDate`
|
|
47
|
+
* (re-probed 2026-08-15; `reportDate` still returns 33.4903). So a caller-supplied
|
|
48
|
+
* `reportDate` is now the only way to reach those indicators, and suppressing the
|
|
49
|
+
* `tradeDate` injection is what makes it work rather than merely being tidier.
|
|
50
|
+
* Injecting into an indicator that takes no date at all is harmless —
|
|
49
51
|
* `pty_op_scope` still answers normally with a stray `tradeDate` attached.
|
|
50
52
|
*
|
|
51
53
|
* `sDate` is deliberately NOT here. It is an interval START, not a substitute:
|
package/dist/src/core/errors.js
CHANGED
|
@@ -66,7 +66,8 @@ const ERROR_HINTS = {
|
|
|
66
66
|
"100003": "msg 已指明字段名或取值范围时直接按 msg 改;msg 只说「参数值非法」时多为枚举参数拼写错误(如 --source / --question-category / --answer-important),对照命令 --help 列出的合法值检查。",
|
|
67
67
|
"100004": "检查 --size / --from 是否为非负数且未超单页上限。",
|
|
68
68
|
"100005": "对照命令 --help 列出的合法取值检查。",
|
|
69
|
-
|
|
69
|
+
// 2026-08-14 起 list 类端点把单页上限(50)也归到此码,不再只是「一次要太多行」。
|
|
70
|
+
"100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI 自动翻页,直发接口时要自己遵守)。",
|
|
70
71
|
// 按参数名判断,不要按命令组:AI 下 management-discuss 的 --report-date 是 date,
|
|
71
72
|
// 而同属 AI 的 knowledge-batch 收时间戳或 datetime——旧文案笼统写"AI 用 datetime"会把
|
|
72
73
|
// --report-date 的用户越导越错。
|
|
@@ -77,8 +78,10 @@ const ERROR_HINTS = {
|
|
|
77
78
|
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|
|
78
79
|
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
79
80
|
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
// 2026-08-14 起下载类把「file-type 非法」拆到 130005、「资源未生成」拆到 130003,
|
|
82
|
+
// 但历史上两者都归过此码,所以提示保留对 --file-type 的指引。
|
|
83
|
+
"130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法。",
|
|
84
|
+
"130003": "资源未生成或该条记录未附带文件——换一条有附件的记录(列表里 hasAttachment/hasFile 为 true 的),或稍后再试。",
|
|
82
85
|
// 下载类命令各有各的 ID 参数(--report-id / --announcement-id / --chunk-id /
|
|
83
86
|
// --summary-id / --conference-id / --record-id / --file-id / --article-id /
|
|
84
87
|
// --independent-opinion-id);--data-id 是异步 *-check 用的,不产生此码。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isVerbose, PAGE_CONCURRENCY, runWithConcurrency } from "./transport.js";
|
|
2
2
|
const DAY_MS = 86_400_000;
|
|
3
|
-
/** API-side row cap (per docs). Used to lift the default 6000-row cap on
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
/** API-side row cap (per docs). Used to lift the default 6000-row cap on whole-market
|
|
4
|
+
* queries so a 2-day A-share shard (~11K rows) isn't silently truncated. Single-security
|
|
5
|
+
* queries are untouched. */
|
|
6
6
|
const ALL_MARKET_LIMIT = 10_000;
|
|
7
7
|
function parseDate(value) {
|
|
8
8
|
// Accept yyyy-MM-dd; reject anything else so we can fall back to a single request.
|
|
@@ -22,11 +22,6 @@ export function isFullMarket(body, fullMarketValue) {
|
|
|
22
22
|
return false;
|
|
23
23
|
return list[0] === fullMarketValue;
|
|
24
24
|
}
|
|
25
|
-
/** Kline uses `all` as its whole-market keyword; fund-flow uses `aShares`. Thin wrapper
|
|
26
|
-
* so kline call sites read naturally. */
|
|
27
|
-
export function isAllMarket(body) {
|
|
28
|
-
return isFullMarket(body, "all");
|
|
29
|
-
}
|
|
30
25
|
/** Sat/Sun in UTC — shard dates are formatted from UTC midnight (see parseDate). */
|
|
31
26
|
function isWeekendUtc(d) {
|
|
32
27
|
const day = d.getUTCDay();
|
|
@@ -40,9 +35,14 @@ function buildShards(start, end, shardDays) {
|
|
|
40
35
|
const shardEnd = Math.min(cursor + (shardDays - 1) * DAY_MS, endTime);
|
|
41
36
|
// Per-day sharding (shardDays===1): a lone weekend day always returns empty (A/HK/US
|
|
42
37
|
// markets closed) — skip it to save ~28% of requests and daily quota. This covers
|
|
43
|
-
// every 1-day-sharded full-market query: fund-flow AND day-kline /
|
|
44
|
-
//
|
|
45
|
-
//
|
|
38
|
+
// every 1-day-sharded full-market query: fund-flow AND day-kline (aShares/usStocks)
|
|
39
|
+
// / day-kline-us.
|
|
40
|
+
//
|
|
41
|
+
// Multi-day shards (day-kline hkStocks=2, index-day-kline=15) are NOT filtered. Note
|
|
42
|
+
// this is a deliberate simplification, not a claim that they always contain a weekday:
|
|
43
|
+
// a 2-day shard starting on a Saturday is Sat+Sun and returns nothing. That costs one
|
|
44
|
+
// wasted request at a range boundary and never drops a trading day, whereas filtering
|
|
45
|
+
// multi-day windows correctly would mean walking each window's days.
|
|
46
46
|
if (!(shardDays === 1 && isWeekendUtc(new Date(cursor)))) {
|
|
47
47
|
shards.push({
|
|
48
48
|
startDate: formatDate(new Date(cursor)),
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated — DO NOT EDIT
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.34.0";
|