gangtise-openapi-cli 0.31.0 → 0.32.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 +15 -7
- package/dist/src/cli.js +76 -59
- package/dist/src/core/args.js +0 -22
- package/dist/src/core/client.js +4 -3
- package/dist/src/core/commandBodies.js +11 -6
- package/dist/src/core/endpoints.js +18 -0
- package/dist/src/core/errors.js +6 -1
- package/dist/src/core/indicatorMatrix.js +31 -22
- package/dist/src/core/printer.js +6 -10
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +38 -17
- package/gangtise-openapi/references/commands/indicator.md +106 -42
- package/gangtise-openapi/references/commands/insight.md +51 -1
- package/gangtise-openapi/references/commands/reference-and-lookup.md +4 -4
- package/gangtise-openapi/references/commands/vault.md +2 -2
- package/gangtise-openapi/references/examples.md +29 -3
- package/gangtise-openapi/references/lookup-ids.md +2 -2
- package/gangtise-openapi/references/response-schema.md +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
README 仅列最近 5 个版本摘要:
|
|
8
8
|
|
|
9
|
+
- **v0.32.0 — 2026-08-08**:新增帕米尔专家纪要列表与下载;跟进 2026-08-07 服务端修复——EDE 缺数据不再整列/整行消失(退出码 3 现在专指「代码没被识别」),条件选股重复指标的 `unreliable` 告警随之移除;`--search-type`/`--rank-type`/`--file-type` 等枚举改为本地拦截(非法值曾被服务端静默忽略并返回全量)。
|
|
9
10
|
- **v0.31.0 — 2026-08-03**:修复 v0.30.1 的矩阵维度校验误杀「单指标 × 板块 ID」时序查询的回归;EDE 结果不完整或不可信时改以退出码 3 标记(`partial` / `unreliable`)。
|
|
10
11
|
- **v0.30.1 — 2026-08-02**:修复 `sDate` 吞掉查询日期导致的区间指标静默错数,让条件选股的文本筛选真正可用,并在服务端整行/整列丢数据时给出警告。
|
|
11
12
|
- **v0.30.0 — 2026-08-02**:适配 EDE 接口重构(`universe` 取代 `securityCodeList`、截面矩阵转置、指标元数据结构化),新增 `indicator screener` 条件选股,并修正复权参数名。
|
|
12
13
|
- **v0.29.0 — 2026-07-25**:新增财报日历与 PDF 解析工具,群消息补 `quoteMsg`,并加强大整数 ID 与高积分调用防护。
|
|
13
|
-
- **v0.28.3 — 2026-07-24**:修复列式响应在错误 `--field` 下静默错列的问题,并校正 EDE 与字段文档。
|
|
14
14
|
|
|
15
15
|
### 历史里程碑
|
|
16
16
|
|
|
@@ -144,6 +144,7 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
144
144
|
| **Lookup** | `broker-org list` / `meeting-org list` | 券商/会议机构本地全量枚举表(按名称找 ID 优先 `reference institution-search`;行业/区域/公告分类/题材/申万码已改用 Reference 接口) |
|
|
145
145
|
| **Insight** | `opinion list` | 内资机构观点 |
|
|
146
146
|
| | `summary list` / `download` | 纪要(含下载,支持 `--file-type` 选原始/HTML) |
|
|
147
|
+
| | `pamirs-summary list` / `download` | 帕米尔专家纪要(需单独购买专家纪要库;筛选项比 `summary` 少,无 `--source`/`--institution`/`--participant-role`) |
|
|
147
148
|
| | `roadshow list` | 路演 |
|
|
148
149
|
| | `site-visit list` | 调研 |
|
|
149
150
|
| | `strategy list` | 策略 |
|
|
@@ -257,7 +258,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
257
258
|
- **HTTP keep-alive**:所有请求复用同一个 `undici.Agent`(连接池 16),避免重复 TLS 握手。
|
|
258
259
|
- **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
|
|
259
260
|
- **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
|
|
260
|
-
- **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**贵档端点例外**(one-pager
|
|
261
|
+
- **自动重试**: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` 单元格,空表另表示整轴 code 未识别)。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
|
|
261
262
|
- **Token 自愈**:调用返回 `0000001008` / `999002` 时自动强制刷新 Token 并重试一次。
|
|
262
263
|
- **K线/资金流向自动分片**:`quote day-kline --security all`、`quote fund-flow --security aShares` 等全市场查询自动按日期切分(A股 K线/资金流向 1 天/片、美股 1 天/片、HK 2 天/片、指数 30 天/片),并发执行后合并结果;按日分片自动跳过周六日。分片时如果用户未传 `--limit`,自动注入 `limit: 10000`(API 上限)避免默认 6000 截断。
|
|
263
264
|
- **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
|
|
@@ -268,6 +269,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
268
269
|
以下列表接口会自动翻页:
|
|
269
270
|
- `insight opinion list`
|
|
270
271
|
- `insight summary list`
|
|
272
|
+
- `insight pamirs-summary list`
|
|
271
273
|
- `insight roadshow list`
|
|
272
274
|
- `insight site-visit list`
|
|
273
275
|
- `insight strategy list`
|
|
@@ -297,12 +299,12 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
297
299
|
- `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
|
|
298
300
|
- 安全上限:自动翻页最多 1000 页,防止异常循环
|
|
299
301
|
- 部分页失败、或服务端实际返回行数与 `total` 矛盾(提前短页)时,不丢弃已取到的数据:结果带 `partial: true`(页失败时另有 `failedPages`;K线分片为 `failedShards`;`--format json` 可见),stderr 输出警告,**进程退出码为 3**(完整成功为 0)
|
|
300
|
-
-
|
|
302
|
+
- **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。**v0.32.0 起这个信号的含义变窄了**——2026-08-07 服务端改为给缺数据补 `null`(行列都保留),所以整列/整行消失现在只发生在**服务端解析不了那个 code** 时(指标码拼错,或证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O`)。真实的无数据/无覆盖是 `null` 单元格 + 退出码 0。⚠️ **这个检测需要同批里有一个能解析的对照物**:拼错的 code 与正确的 code 混在一批才会标 partial;**整个轴都写错时(如只查一个拼错的指标)响应是空表、退出码 0**,只有 stderr 提示——空表拿到手必须先核对代码拼写与后缀。同版本移除了条件选股重复指标的 `unreliable` / `duplicatedIndicators` 标记(服务端已修复,继续告警会是误报)。**条件选股的缺列另有更严的一档**:把缺列的变量当作无法求值,若表达式(按 `&&`/`||` 的布尔结构)再无任何可成立的分支,则**退出码 1 且不输出**——那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过。⚠️ 这一档以「服务端返回了命中行」为前提;**零命中时一律退出码 0**(没有行需要被质疑),所以空集要先核对指标码拼写,不能直接当成「无标的符合条件」。语义约定:`0` 完整成功(含合法空结果)/`3` 有数据但不完整/`1` 硬失败
|
|
301
303
|
- 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`);其他格式下 stderr 输出 `Total: N, showing: M`(json 格式不输出该行)
|
|
302
304
|
|
|
303
305
|
## 智能文件命名
|
|
304
306
|
|
|
305
|
-
下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`announcement-hk download`、`announcement-us download`、`official-account download`、`performance-calendar download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
307
|
+
下载命令(`summary download`、`pamirs-summary download`、`research download`、`foreign-report download`、`announcement download`、`announcement-hk download`、`announcement-us download`、`official-account download`、`performance-calendar download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
306
308
|
|
|
307
309
|
1. **缓存优先** — 如果之前执行过对应的 `list` 命令,标题已缓存在 `~/.config/gangtise/title-cache.json`,直接使用,无额外 API 调用
|
|
308
310
|
2. **API 回查** — 缓存未命中时,自动查询最近 200 条记录匹配标题
|
|
@@ -334,6 +336,12 @@ gangtise insight research list --broker C100000027 --broker C100000014 --industr
|
|
|
334
336
|
gangtise insight opinion list --keyword AI
|
|
335
337
|
gangtise insight summary list --keyword 算力
|
|
336
338
|
|
|
339
|
+
# 帕米尔专家纪要(需单独购买专家纪要库;全文搜索 + 时间倒序)
|
|
340
|
+
# --rank-type 2 = 严格时间倒序;换成 1(综合排序)在「全文搜索 + 有关键词」下是真正的相关度重排
|
|
341
|
+
gangtise insight pamirs-summary list --keyword PCB --search-type 2 --rank-type 2 --size 20
|
|
342
|
+
gangtise insight pamirs-summary download --summary-id 5863771 --file-type 2
|
|
343
|
+
# → PCB钻针:高端钻针扩产有壁垒,供需紧缺会持续到28年.html
|
|
344
|
+
|
|
337
345
|
# 下载:先 list 再 download,自动使用真实标题作为文件名
|
|
338
346
|
gangtise insight summary download --summary-id 4902586
|
|
339
347
|
# → 超颖电子:2026年4月7日投资者关系活动记录表.txt
|
|
@@ -705,7 +713,7 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
|
|
|
705
713
|
| `999010` | 接口地址不存在(`raw call` 的 key 可能已下线,用 `raw list` 核对) |
|
|
706
714
|
| `999012` / `999013` / `999014` | 账号禁用 / 已过期 / 租户失效 |
|
|
707
715
|
| `999016` | 调用方 IP 不在允许范围 |
|
|
708
|
-
| `999999` | Gangtise 系统错误,请稍后重试(`indicator`
|
|
716
|
+
| `999999` | Gangtise 系统错误,请稍后重试(`indicator` 端点的「无数据」已不再用此码——有效 code 无数据返回 `null` 单元格,此码基本只剩真故障) |
|
|
709
717
|
| `140002` | 终态失败:AI 异步生成失败,或 `indicator` 的参数/表达式错误(枚举越界、语法错)——改参数重提,不重试 |
|
|
710
718
|
| `100003` | 参数值非法——**最宽的兜底码**;msg 通常已指明字段(如「limit 最小为 1,最大为 10000」),先读 msg |
|
|
711
719
|
| `100001` | 缺必填参数(msg 带字段名,如「缺少必填参数: reportId」) |
|
|
@@ -717,11 +725,11 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
|
|
|
717
725
|
| `410110` / `410111` | 异步任务生成中(继续轮询)/ 生成失败(终态)——**实测服务端仍发这两个旧码**;新码 `140001`/`140002`,CLI 两代都认 |
|
|
718
726
|
| `240001` | 财报期未披露或超出查询期(`earnings-review` 提交阶段即报,不扣积分) |
|
|
719
727
|
| `250001` | 不支持该数据源(`knowledge-resource-download` 需正确的 `resourceType + sourceId` 组合)——**取代旧 `433007`** |
|
|
720
|
-
| `900002` | 请求方法不正确(服务端 msg
|
|
728
|
+
| `900002` | 请求方法不正确(服务端 msg 为「请求类型有误」,HTTP 405) |
|
|
721
729
|
|
|
722
730
|
> **关于这次错误码重排**:服务端 2026-07-17 重排了 41 个公开码(三层:`999xxx` 服务统一层 / `1xxxxx` 业务通用层 / `2xxxxx` 接口专有层),信封新增 `errorType` 和 `traceId`。2026-07-20 逐码实测发现**迁移是按「错误处理层」而非按业务模块进行的**:同一个接口内,参数校验层与路由层已发新码,方法路由层、token 过滤器、以及异步生成状态仍发旧码。新码信封 `code` 是 JSON 数字且带 `errorType`,旧码是字符串且没有——但这判断的是单条错误路径,不是整个接口;CLI 对两代都能识别。报错行会带 `[trace <id>]`,**报障时请带上它**。
|
|
723
731
|
>
|
|
724
|
-
> 其余码(`999003`–`999006`、`999012`–`999016`、`100002`、`100004`、`100005`、`
|
|
732
|
+
> 其余码(`999003`–`999006`、`999012`–`999016`、`100002`、`100004`、`100005`、`130003`–`130005`、`210001`、`220001`、`230001`、`240002`、`240003`)在实测中未触发到,多被上面的兜底码接管,CLI 仍内置了对应提示。⚠️ `110003`(超出时间范围限制)此前也列在这里,**2026-08-08 实测已确认可触发**——凡超出账号数据权限范围的查询都返回它(如 `fundamental income-statement --fiscal-year 2015`)。两个实测坑:**枚举值拼错和分页越界服务端不报错**(静默忽略该筛选条件,v0.32.0 起 CLI 对 `--search-type`/`--rank-type`/`--file-type` 等已知枚举本地拦截);**`viewpoint-debate` 的敏感内容不会被提前拦截**,会扣满 50 积分再以 `410111` 失败。
|
|
725
733
|
|
|
726
734
|
---
|
|
727
735
|
|
package/dist/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
3
|
import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/asyncContent.js";
|
|
4
4
|
import { readTokenCache, redactTokenCache } from "./core/auth.js";
|
|
5
|
-
import { collectKeyValue, collectList, collectNumberList, dateArg, datetimeArg,
|
|
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
8
|
import { callKlineWithSharding, isAllMarket, isFullMarket } from "./core/quoteSharding.js";
|
|
@@ -87,10 +87,10 @@ async function runDownload(client, endpointKey, query, options) {
|
|
|
87
87
|
*/
|
|
88
88
|
function addDownloadCommand(parent, spec) {
|
|
89
89
|
const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`);
|
|
90
|
-
if (spec.fileType
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if (spec.fileType) {
|
|
91
|
+
const option = new Option("--file-type <number>", spec.fileType.description).choices(spec.fileType.choices);
|
|
92
|
+
cmd.addOption(spec.fileType.required ? option.makeOptionMandatory() : option.default(spec.fileType.default));
|
|
93
|
+
}
|
|
94
94
|
if (spec.contentTypeDescription)
|
|
95
95
|
cmd.requiredOption("--content-type <type>", spec.contentTypeDescription);
|
|
96
96
|
cmd.option("--output <path>").action((options) => withClient(async (client) => {
|
|
@@ -158,6 +158,7 @@ program.addCommand(lookup);
|
|
|
158
158
|
const insight = new Command("insight").description("Insight APIs");
|
|
159
159
|
const opinion = new Command("opinion");
|
|
160
160
|
const summary = new Command("summary");
|
|
161
|
+
const pamirsSummary = new Command("pamirs-summary");
|
|
161
162
|
const roadshow = new Command("roadshow");
|
|
162
163
|
const siteVisit = new Command("site-visit");
|
|
163
164
|
const strategy = new Command("strategy");
|
|
@@ -173,19 +174,39 @@ const independentOpinion = new Command("independent-opinion");
|
|
|
173
174
|
const officialAccount = new Command("official-account");
|
|
174
175
|
const qa = new Command("qa");
|
|
175
176
|
const reportImage = new Command("report-image");
|
|
176
|
-
addTimeFilters(opinion.command("list").
|
|
177
|
+
addTimeFilters(opinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
|
|
177
178
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
178
179
|
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
|
|
179
180
|
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
|
|
180
181
|
llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
|
|
181
182
|
})));
|
|
182
|
-
addTimeFilters(summary.command("list").
|
|
183
|
+
addTimeFilters(summary.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.summary.list", {
|
|
183
184
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
184
185
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
|
|
185
186
|
researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
|
|
186
187
|
categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
|
|
187
188
|
}), { endpointKey: "insight.summary.list", idField: "summaryId" }));
|
|
188
|
-
addDownloadCommand(summary, { endpointKey: "insight.summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "summary", fileType: { description: "File type: 1=original(default) 2=HTML; only affects meeting platform summaries" }, titleListEndpoint: "insight.summary.list" });
|
|
189
|
+
addDownloadCommand(summary, { endpointKey: "insight.summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "summary", fileType: { description: "File type: 1=original(default) 2=HTML; only affects meeting platform summaries", choices: ["1", "2"] }, titleListEndpoint: "insight.summary.list" });
|
|
190
|
+
// Pamirs is one lead institution's expert-summary library, exposed on its own
|
|
191
|
+
// path rather than as a `summary list` filter. It advertises a NARROWER filter
|
|
192
|
+
// set than `summary` — no --source / --institution / --participant-role — so the
|
|
193
|
+
// options are spelled out here instead of sharing summary's builder: an
|
|
194
|
+
// unsupported flag would be dropped server-side and silently widen the result.
|
|
195
|
+
// Every enum is whitelisted locally. The server drops an unrecognised VALUE the
|
|
196
|
+
// same way it drops an unrecognised FIELD — silently, returning the unfiltered
|
|
197
|
+
// set with exit 0. Worse, a bad `--search-type` takes `--keyword` down with it:
|
|
198
|
+
// `--keyword 茅台 --search-type 99` answers 2963 (the whole library) instead of 2,
|
|
199
|
+
// so the caller reads a full-library dump as a keyword hit. Same class of defect
|
|
200
|
+
// that put whitelists on securities-search / institution-search / official-account.
|
|
201
|
+
const PAMIRS_CATEGORIES = ["companyAnalysis", "industryAnalysis"];
|
|
202
|
+
const PAMIRS_MARKETS = ["aShares", "hkStocks", "usChinaConcept", "usStocks"];
|
|
203
|
+
addTimeFilters(pamirsSummary.command("list").description("List Pamirs expert summaries (requires the expert-summary database)").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID; unlike most insight lists this accepts BOTH citicIndustry (1008001xx) and swIndustry (104xx0000) codes", collectList, []).option("--security <code>", "Security code, e.g. 000001.SZ", collectList, []).option("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, collectList, []).option("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.pamirs-summary.list", {
|
|
204
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
205
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
|
|
206
|
+
researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security),
|
|
207
|
+
categoryList: parseChoiceList(options.category, "--category", PAMIRS_CATEGORIES), marketList: parseChoiceList(options.market, "--market", PAMIRS_MARKETS),
|
|
208
|
+
}), { endpointKey: "insight.pamirs-summary.list", idField: "summaryId" }));
|
|
209
|
+
addDownloadCommand(pamirsSummary, { endpointKey: "insight.pamirs-summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "pamirs-summary", fileType: { description: "File type: 1=original(default) 2=HTML", choices: ["1", "2"] }, titleListEndpoint: "insight.pamirs-summary.list" });
|
|
189
210
|
const addScheduleList = (command, endpointKey, fields) => {
|
|
190
211
|
const list = command.command("list");
|
|
191
212
|
if (fields.researchArea)
|
|
@@ -310,7 +331,7 @@ performanceCalendar.command("list").description("Earnings calendar (业绩预告
|
|
|
310
331
|
}, { endpointKey: "insight.performance-calendar.list", idField: "performanceReportId" });
|
|
311
332
|
});
|
|
312
333
|
addDownloadCommand(performanceCalendar, { endpointKey: "insight.performance-calendar.download", idOption: "--performance-report-id", idField: "performanceReportId", fallbackPrefix: "performance-calendar", titleListEndpoint: "insight.performance-calendar.list" });
|
|
313
|
-
addTimeFilters(research.command("list").
|
|
334
|
+
addTimeFilters(research.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--broker <id>", "Broker ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--source <type>", "Source type", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.research.list", {
|
|
314
335
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
315
336
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
316
337
|
brokerList: maybeArray(options.broker), securityList: maybeArray(options.security), industryList: maybeArray(options.industry),
|
|
@@ -318,8 +339,8 @@ addTimeFilters(research.command("list").option("--search-type <number>", "Search
|
|
|
318
339
|
ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
|
|
319
340
|
maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
|
|
320
341
|
}), { endpointKey: "insight.research.list", idField: "reportId" }));
|
|
321
|
-
addDownloadCommand(research, { endpointKey: "insight.research.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "research", fileType: { description: "File type: 1=PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.research.list" });
|
|
322
|
-
addTimeFilters(foreignReport.command("list").
|
|
342
|
+
addDownloadCommand(research, { endpointKey: "insight.research.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "research", fileType: { description: "File type: 1=PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.research.list" });
|
|
343
|
+
addTimeFilters(foreignReport.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code", collectList, []).option("--region <id>", "Region ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-report.list", {
|
|
323
344
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
324
345
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
325
346
|
securityList: maybeArray(options.security), regionList: maybeArray(options.region), categoryList: maybeArray(options.category),
|
|
@@ -327,21 +348,21 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
|
|
|
327
348
|
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
328
349
|
minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
|
|
329
350
|
}), { endpointKey: "insight.foreign-report.list", idField: "reportId" }));
|
|
330
|
-
addDownloadCommand(foreignReport, { endpointKey: "insight.foreign-report.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "foreign-report", fileType: { description: "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", default: "1" }, titleListEndpoint: "insight.foreign-report.list" });
|
|
351
|
+
addDownloadCommand(foreignReport, { endpointKey: "insight.foreign-report.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "foreign-report", fileType: { description: "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", choices: ["1", "2", "3", "4"], default: "1" }, titleListEndpoint: "insight.foreign-report.list" });
|
|
331
352
|
// Contract: A-share announcement startTime/endTime go out as 13-digit epoch millis
|
|
332
353
|
// (parseTimestamp13), while HK/US announcement and every other insight list send the
|
|
333
354
|
// datetime string straight through. All three filter correctly — verified live against
|
|
334
355
|
// a narrow past window (each returns in-window rows). A-share's API also accepts the
|
|
335
356
|
// string form, but the 13-digit conversion is kept as the historical spec contract;
|
|
336
357
|
// don't "unify" it away without re-confirming the A-share announcement spec.
|
|
337
|
-
addTimeFilters(announcement.command("list").
|
|
358
|
+
addTimeFilters(announcement.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement.list", {
|
|
338
359
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
339
360
|
startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
|
|
340
361
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
|
|
341
362
|
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
342
363
|
}), { endpointKey: "insight.announcement.list", idField: "announcementId" }));
|
|
343
|
-
addDownloadCommand(announcement, { endpointKey: "insight.announcement.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement", fileType: { description: "File type: 1=PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement.list" });
|
|
344
|
-
addTimeFilters(announcementHk.command("list").
|
|
364
|
+
addDownloadCommand(announcement, { endpointKey: "insight.announcement.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement", fileType: { description: "File type: 1=PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement.list" });
|
|
365
|
+
addTimeFilters(announcementHk.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. 01913.HK)", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement-hk.list", {
|
|
345
366
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
346
367
|
startTime: options.startTime, endTime: options.endTime,
|
|
347
368
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
|
|
@@ -349,8 +370,8 @@ addTimeFilters(announcementHk.command("list").option("--search-type <number>", "
|
|
|
349
370
|
keyword: options.keyword,
|
|
350
371
|
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
351
372
|
}), { endpointKey: "insight.announcement-hk.list", idField: "announcementId" }));
|
|
352
|
-
addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", fileType: { description: "File type: 1=original 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement-hk.list" });
|
|
353
|
-
addTimeFilters(announcementUs.command("list").
|
|
373
|
+
addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", fileType: { description: "File type: 1=original 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-hk.list" });
|
|
374
|
+
addTimeFilters(announcementUs.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. TSLA.O)", collectList, []).option("--category <id>", "Category ID (constant-list usShareAnnouncementCategory)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement-us.list", {
|
|
354
375
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
355
376
|
startTime: options.startTime, endTime: options.endTime,
|
|
356
377
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
|
|
@@ -358,8 +379,8 @@ addTimeFilters(announcementUs.command("list").option("--search-type <number>", "
|
|
|
358
379
|
keyword: options.keyword,
|
|
359
380
|
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
360
381
|
}), { endpointKey: "insight.announcement-us.list", idField: "announcementId" }));
|
|
361
|
-
addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
|
|
362
|
-
addTimeFilters(foreignOpinion.command("list").
|
|
382
|
+
addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
|
|
383
|
+
addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-opinion.list", {
|
|
363
384
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
364
385
|
startTime: options.startTime, endTime: options.endTime,
|
|
365
386
|
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
@@ -368,7 +389,7 @@ addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Ra
|
|
|
368
389
|
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
|
|
369
390
|
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
370
391
|
})));
|
|
371
|
-
addTimeFilters(independentOpinion.command("list").
|
|
392
|
+
addTimeFilters(independentOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.independent-opinion.list", {
|
|
372
393
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
373
394
|
startTime: options.startTime, endTime: options.endTime,
|
|
374
395
|
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
@@ -376,8 +397,8 @@ addTimeFilters(independentOpinion.command("list").option("--rank-type <number>",
|
|
|
376
397
|
industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
|
|
377
398
|
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
378
399
|
})));
|
|
379
|
-
addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", required: true } });
|
|
380
|
-
addTimeFilters(officialAccount.command("list").
|
|
400
|
+
addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", choices: ["1", "2"], required: true } });
|
|
401
|
+
addTimeFilters(officialAccount.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--account-id <id>", "Official account ID", collectList, []).option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--category <type>", "Article type: news/law/report/view/data/event/meeting/notice/recruit/investEdu/brand/notes/other", collectList, []).option("--industry <id>", "Industry ID (constant-list citicIndustry/swIndustry)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.official-account.list", {
|
|
381
402
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
382
403
|
startTime: options.startTime, endTime: options.endTime,
|
|
383
404
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
|
|
@@ -386,7 +407,7 @@ addTimeFilters(officialAccount.command("list").option("--search-type <number>",
|
|
|
386
407
|
accountIdList: maybeArray(options.accountId), securityList: maybeArray(options.security),
|
|
387
408
|
categoryList: maybeArray(options.category), industryList: maybeArray(options.industry),
|
|
388
409
|
}), { endpointKey: "insight.official-account.list", idField: "articleId" }));
|
|
389
|
-
addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.download", idOption: "--article-id", idField: "articleId", fallbackPrefix: "official-account", fileType: { description: "File type: 1=txt(default) 2=HTML", default: "1" }, titleListEndpoint: "insight.official-account.list" });
|
|
410
|
+
addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.download", idOption: "--article-id", idField: "articleId", fallbackPrefix: "official-account", fileType: { description: "File type: 1=txt(default) 2=HTML", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.official-account.list" });
|
|
390
411
|
// QA request keys are BARE (source/questionCategory/answerImportant), not the *List
|
|
391
412
|
// convention — the body below mirrors the spec exactly. Datetimes pass through as strings.
|
|
392
413
|
qa.command("list").requiredOption("--security-code <code>", "Security code, e.g. 601012.SH").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all (max page 500)").option("--start-time <datetime>", "Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time")).option("--source <type>", "Source: conference/interactive/survey (repeat)", collectList, []).option("--question-category <name>", "Question category (repeat): productAndBusiness/capacityAndProjects/ordersAndCustomers/financialData/materialEvents/capitalOperations/shareholdersAndDividends/corporateGovernance/marketAndValuation/macroAndIndustry/risksAndOthers", collectList, []).option("--answer-important <flag>", "Answer involves key info: 1=yes 0=no (repeat; omit for all)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("insight.qa.list", {
|
|
@@ -402,6 +423,7 @@ reportImage.command("list").requiredOption("--keyword <text>", "Search keyword,
|
|
|
402
423
|
addDownloadCommand(reportImage, { endpointKey: "insight.report-image.download", idOption: "--chunk-id", idField: "chunkId", fallbackPrefix: "report-image" });
|
|
403
424
|
insight.addCommand(opinion);
|
|
404
425
|
insight.addCommand(summary);
|
|
426
|
+
insight.addCommand(pamirsSummary);
|
|
405
427
|
insight.addCommand(roadshow);
|
|
406
428
|
insight.addCommand(siteVisit);
|
|
407
429
|
insight.addCommand(strategy);
|
|
@@ -697,22 +719,31 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
|
|
|
697
719
|
alternative.command("concept-info").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-info", { conceptId: options.conceptId })));
|
|
698
720
|
alternative.command("concept-securities").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-securities", { conceptId: options.conceptId })));
|
|
699
721
|
program.addCommand(alternative);
|
|
700
|
-
/** Mark and report
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
722
|
+
/** Mark and report the request codes the server did not answer for at all.
|
|
723
|
+
*
|
|
724
|
+
* What this catches changed on the server. EDE used to drop any axis it had no
|
|
725
|
+
* DATA for — an indicator empty for every security vanished from
|
|
726
|
+
* `indicatorList`, a security empty for every indicator vanished from
|
|
727
|
+
* `securityCodeList`. Re-probed 2026-08-08: a coverage gap is now padded with
|
|
728
|
+
* `null` and keeps its row and column (`mgn_bal` × 00700.HK, `finc_pb_mrq` ×
|
|
729
|
+
* 09992.HK — both null, both present, even as the only cell in the request).
|
|
730
|
+
*
|
|
731
|
+
* A code the server cannot RESOLVE still vanishes: an unknown indicator code, or
|
|
732
|
+
* a security code with the wrong suffix (`AAPL.US`, whose real form is
|
|
733
|
+
* `AAPL.O`). So this is now a typo detector rather than a coverage detector —
|
|
734
|
+
* which is the more useful of the two, since a coverage gap is visible as `null`
|
|
735
|
+
* but a misspelled code is otherwise invisible: exit code 0, a plausible-looking
|
|
736
|
+
* table, and a `--key-by code` mapping whose key simply is not there.
|
|
706
737
|
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
738
|
+
* Same signal as a failed page or a row cap: `partial` on the payload (printData
|
|
739
|
+
* → exit 3) plus the omitted codes, so an automated caller can react without
|
|
740
|
+
* parsing stderr. */
|
|
710
741
|
function flagDropped(rows, data, requestedSecurities, requestedIndicators) {
|
|
711
|
-
// A wholly empty response is
|
|
712
|
-
//
|
|
713
|
-
//
|
|
742
|
+
// A wholly empty response is not a partial one: the diff against the request
|
|
743
|
+
// would list everything as "omitted", which says nothing about which axis is
|
|
744
|
+
// at fault. Exit 0, but say why it is ambiguous.
|
|
714
745
|
if (isEmptyMatrix(data)) {
|
|
715
|
-
process.stderr.write("[gangtise] note: the query returned no data at all.
|
|
746
|
+
process.stderr.write("[gangtise] note: the query returned no data at all. Since a real coverage gap now comes back as a null cell rather than an empty table, this usually means NOTHING in the request resolved — every security code or every indicator code was unrecognised — or a parameter name is wrong. Cross-check codes against 'gangtise indicator search --format json' and 'gangtise reference securities-search'.\n");
|
|
716
747
|
return;
|
|
717
748
|
}
|
|
718
749
|
const { securities, indicators } = droppedFromMatrix(data, requestedSecurities, requestedIndicators);
|
|
@@ -731,20 +762,7 @@ function flagDropped(rows, data, requestedSecurities, requestedIndicators) {
|
|
|
731
762
|
parts.push(`indicators ${indicators.join(", ")}`);
|
|
732
763
|
if (securities.length > 0)
|
|
733
764
|
parts.push(`securities ${securities.join(", ")}`);
|
|
734
|
-
process.stderr.write(`[gangtise] warning: the response omits ${parts.join(" and ")} entirely
|
|
735
|
-
}
|
|
736
|
-
/** Mark a screener result whose values cannot be trusted. Distinct from
|
|
737
|
-
* `partial`: no rows are missing — the numbers themselves may belong to the
|
|
738
|
-
* wrong variable. A duplicated indicator code is resolved from the EARLIEST date
|
|
739
|
-
* among its bindings, that single value lands in the first of their columns, and
|
|
740
|
-
* the rest come back null (probed 2026-08-03), so both the values shown and the
|
|
741
|
-
* set of matched securities are untrustworthy. */
|
|
742
|
-
function flagUnreliable(rows, duplicated) {
|
|
743
|
-
if (rows && typeof rows === "object" && !Array.isArray(rows)) {
|
|
744
|
-
const rec = rows;
|
|
745
|
-
rec.unreliable = true;
|
|
746
|
-
rec.duplicatedIndicators = duplicated;
|
|
747
|
-
}
|
|
765
|
+
process.stderr.write(`[gangtise] warning: the response omits ${parts.join(" and ")} entirely — no row/column at all, not a null one. A code the server merely has no data for still comes back as null, so this normally means the code itself was not recognised: check it for typos and, for securities, for the wrong market suffix (US tickers are .O/.N, not .US). Result marked partial (exit 3).\n`);
|
|
748
766
|
}
|
|
749
767
|
/** `--indicator` / `--security` are repeatable, so Commander cannot mark them
|
|
750
768
|
* required — but every matrix endpoint needs at least one of each and answers a
|
|
@@ -766,7 +784,7 @@ indicator.command("search").requiredOption("--keyword <text>", "Search keyword,
|
|
|
766
784
|
});
|
|
767
785
|
await printData(unwrapIndicatorData(raw), format, options.output);
|
|
768
786
|
}));
|
|
769
|
-
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
|
|
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 — the server resolves that to the enclosing report period for report-period indicators, or pass --indicator-param 'code:reportDate=...' to be explicit", 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) => {
|
|
770
788
|
const format = parseOutputFormat(options.format);
|
|
771
789
|
requireIndicatorScope(options.indicator, options.security);
|
|
772
790
|
const raw = await client.call("indicator.cross-section", buildIndicatorCrossSectionBody(options));
|
|
@@ -794,11 +812,12 @@ indicator.command("time-series").option("--indicator <code>", "Indicator code, e
|
|
|
794
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) => {
|
|
795
813
|
const format = parseOutputFormat(options.format);
|
|
796
814
|
requireIndicatorScope(options.indicator, options.security);
|
|
815
|
+
// Binding one indicator code to several variables (the same price on two
|
|
816
|
+
// dates) is supported: the server used to answer every such binding from the
|
|
817
|
+
// EARLIEST date among them and null the rest, which made the whole result
|
|
818
|
+
// untrustworthy and needed an `unreliable` flag. Re-probed 2026-08-08 — fixed:
|
|
819
|
+
// each variable now carries its own date's value, stable across repeats.
|
|
797
820
|
const bindings = parseScreenerIndicators(options.indicator, options.indicatorParam, options.expression);
|
|
798
|
-
const duplicated = duplicateScreenerCodes(bindings);
|
|
799
|
-
if (duplicated.length > 0) {
|
|
800
|
-
process.stderr.write(`[gangtise] warning: ${duplicated.join(", ")} bound to more than one variable — the API allows this, but the server currently resolves ALL of those bindings against the EARLIEST date among them, puts that single value in the first of their columns, and leaves the rest null (probed 2026-08-03). So the surviving number is not necessarily the variable it is labelled with: a variable asking for 07-31 came back holding 07-30's price. Every part of the result is affected — the values shown, and which securities matched at all, since conditions on the null variables filtered on nothing. The same request also returns an empty set about a third of the time. Treat the WHOLE result as unusable and split into separate calls until it is fixed.\n`);
|
|
801
|
-
}
|
|
802
821
|
const raw = await client.call("indicator.screener", buildIndicatorScreenerBody(options));
|
|
803
822
|
// Same payload shape as cross-section (one row per matched security, one
|
|
804
823
|
// column per indicator) with a `field` on each indicator entry. No dropped-row
|
|
@@ -825,7 +844,7 @@ indicator.command("screener").description("Screen securities by an expression ov
|
|
|
825
844
|
// flattenCrossSection above already asserted this is an array of non-empty
|
|
826
845
|
// strings, so only its length is left to read.
|
|
827
846
|
if (data.securityCodeList.length === 0) {
|
|
828
|
-
process.stderr.write("[gangtise] note: nothing matched the expression. That is a normal answer — but
|
|
847
|
+
process.stderr.write("[gangtise] note: nothing matched the expression. That is a normal answer — but an empty result is ALSO what an unrecognised code or a wrong parameter name produces. Cross-check the indicator codes and parameters against 'gangtise indicator search --format json'.\n");
|
|
829
848
|
}
|
|
830
849
|
if (unbound.length > 0) {
|
|
831
850
|
// Whatever reached here still leaves the expression evaluable (or was never
|
|
@@ -838,10 +857,8 @@ indicator.command("screener").description("Screen securities by an expression ov
|
|
|
838
857
|
const filterNote = alsoFiltered.length > 0
|
|
839
858
|
? ` The expression also filters on ${alsoFiltered.join(", ")}, so that condition was applied to none of these rows — another branch of the expression could still have matched them legitimately, but verify before relying on that filter.`
|
|
840
859
|
: "";
|
|
841
|
-
process.stderr.write(`[gangtise] warning: ${unbound.join(", ")} produced no column at all
|
|
860
|
+
process.stderr.write(`[gangtise] warning: ${unbound.join(", ")} produced no column at all — those output values are missing. An indicator the server merely has no data for still returns a null column, so this normally means the bound code was not recognised; check it against 'gangtise indicator search'.${filterNote} Result marked partial (exit 3).\n`);
|
|
842
861
|
}
|
|
843
|
-
if (duplicated.length > 0)
|
|
844
|
-
flagUnreliable(rows, duplicated);
|
|
845
862
|
await printData(rows, format, options.output);
|
|
846
863
|
}));
|
|
847
864
|
program.addCommand(indicator);
|
package/dist/src/core/args.js
CHANGED
|
@@ -421,25 +421,3 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
|
421
421
|
}
|
|
422
422
|
return [...indicators.values()];
|
|
423
423
|
}
|
|
424
|
-
/** Indicator codes bound to more than one screener variable, in first-seen
|
|
425
|
-
* order. The API is specified to allow this (one code under two variables with
|
|
426
|
-
* different parameters), but the server currently mis-resolves it — probed
|
|
427
|
-
* 2026-08-03: ALL such bindings are answered from the EARLIEST date among them,
|
|
428
|
-
* that one value lands in the first of their columns, and the rest come back
|
|
429
|
-
* null. The surviving number therefore need not belong to the variable it is
|
|
430
|
-
* labelled with, so it is not merely "one column is missing": the values shown
|
|
431
|
-
* and the set of matched securities are both untrustworthy. (The same request
|
|
432
|
-
* also returns an empty set roughly a third of the time.)
|
|
433
|
-
*
|
|
434
|
-
* Callers warn rather than reject: the capability is intended and a server-side
|
|
435
|
-
* fix is in flight, so a hard block here would have to be reverted. */
|
|
436
|
-
export function duplicateScreenerCodes(indicators) {
|
|
437
|
-
const seen = new Set();
|
|
438
|
-
const duplicated = new Set();
|
|
439
|
-
for (const { indicatorCode } of indicators) {
|
|
440
|
-
if (seen.has(indicatorCode))
|
|
441
|
-
duplicated.add(indicatorCode);
|
|
442
|
-
seen.add(indicatorCode);
|
|
443
|
-
}
|
|
444
|
-
return [...duplicated];
|
|
445
|
-
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -427,8 +427,9 @@ export class GangtiseClient {
|
|
|
427
427
|
},
|
|
428
428
|
}).catch((error) => {
|
|
429
429
|
// Until 2026-08-01 EDE answered a no-data query with 999999, so this hint
|
|
430
|
-
// led with "多为查询无数据". No-data now comes back as
|
|
431
|
-
//
|
|
430
|
+
// led with "多为查询无数据". No-data now comes back as a null CELL with its
|
|
431
|
+
// row and column intact (re-probed 2026-08-08), which leaves 999999 meaning
|
|
432
|
+
// an actual server fault — but the parameter checklist
|
|
432
433
|
// is still the right first move, because a wrong param name or date axis
|
|
433
434
|
// silently yields an EMPTY TABLE rather than this error. Only the data
|
|
434
435
|
// endpoints take a date/security/params; indicator.search shares the
|
|
@@ -436,7 +437,7 @@ export class GangtiseClient {
|
|
|
436
437
|
// instead of nonsensical date/scope/param guidance.
|
|
437
438
|
const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
|
|
438
439
|
if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
|
|
439
|
-
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名是静默失效)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required
|
|
440
|
+
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名是静默失效)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——有效 code 无数据现在返回保留行列的 null 单元格,空表则多半是整个轴的 code 都没被识别。');
|
|
440
441
|
}
|
|
441
442
|
throw error;
|
|
442
443
|
});
|
|
@@ -36,12 +36,17 @@ export function buildStockPoolStocksBody(options) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Parameter keys that REPLACE an indicator's `tradeDate`. The 2026-08-01 EDE
|
|
38
38
|
* revision dropped the root-level `date`, so `--date` now fans out into each
|
|
39
|
-
* indicator's `tradeDate`.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
39
|
+
* indicator's `tradeDate`. A caller-supplied `reportDate` suppresses that
|
|
40
|
+
* injection for its indicator, so the two date fields are never sent together.
|
|
41
|
+
*
|
|
42
|
+
* This used to be load-bearing in a stronger sense: a report-period indicator
|
|
43
|
+
* answered a `tradeDate` with an EMPTY result rather than an error (probed
|
|
44
|
+
* 2026-08-01 on `is_op_rev_mom`). Re-probed 2026-08-08 — the server now resolves
|
|
45
|
+
* a `tradeDate` to the enclosing report period, and `is_op_rev_mom` returns the
|
|
46
|
+
* same 33.4903 either way. Kept regardless: sending the caller's own date field
|
|
47
|
+
* rather than a second one alongside it is the honest request, and it survives a
|
|
48
|
+
* rollback. Injecting into an indicator that takes no date at all is harmless —
|
|
49
|
+
* `pty_op_scope` still answers normally with a stray `tradeDate` attached.
|
|
45
50
|
*
|
|
46
51
|
* `sDate` is deliberately NOT here. It is an interval START, not a substitute:
|
|
47
52
|
* `qte_vol_intvl` declares `tradeDate` required (the interval END) and `sDate`
|
|
@@ -49,6 +49,24 @@ const ENDPOINT_DEFS = {
|
|
|
49
49
|
// 50/篇 — same price tier as the AI Agent calls; billing probed non-idempotent.
|
|
50
50
|
retry: "no-replay",
|
|
51
51
|
},
|
|
52
|
+
"insight.pamirs-summary.list": {
|
|
53
|
+
method: "POST",
|
|
54
|
+
path: "/application/open-insight/pamirs-summary/getList",
|
|
55
|
+
kind: "json",
|
|
56
|
+
description: "List Pamirs expert summaries (requires the expert-summary database)",
|
|
57
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
58
|
+
},
|
|
59
|
+
"insight.pamirs-summary.download": {
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: "/application/open-insight/pamirs-summary/download/file",
|
|
62
|
+
kind: "download",
|
|
63
|
+
description: "Download a Pamirs expert summary file",
|
|
64
|
+
// The 2026-08-07 spec states an entitlement (the expert-summary database) but
|
|
65
|
+
// no per-call price. Treated as non-idempotent anyway, like its
|
|
66
|
+
// `insight.summary.download` sibling: if it does meter, a 5xx replay
|
|
67
|
+
// double-bills, and the only cost of being wrong is losing one retry.
|
|
68
|
+
retry: "no-replay",
|
|
69
|
+
},
|
|
52
70
|
"insight.roadshow.list": {
|
|
53
71
|
method: "POST",
|
|
54
72
|
path: "/application/open-insight/schedule/roadshow/getList",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -72,7 +72,9 @@ const ERROR_HINTS = {
|
|
|
72
72
|
// --report-date 的用户越导越错。
|
|
73
73
|
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
74
74
|
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
75
|
-
|
|
75
|
+
// 超出的是账号数据权限的时间下界,不是单次窗口太宽——`--fiscal-year 2015` 这种
|
|
76
|
+
// 整个区间都在界外的请求,怎么缩窗口都还是这个码(实测 2026-08-08)。
|
|
77
|
+
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|
|
76
78
|
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
77
79
|
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
78
80
|
"130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法(非法 file-type 也归此码)。",
|
|
@@ -91,6 +93,9 @@ const ERROR_HINTS = {
|
|
|
91
93
|
"210001": "换一篇,或改用 list 取正文摘要。",
|
|
92
94
|
"220001": "改用 list 取正文摘要。",
|
|
93
95
|
"230001": "只有自己上传的文件可下载。",
|
|
96
|
+
// 2026-08-07 新增码。私域模块,`vault wechat-*` 就在这个模块下且明确要求
|
|
97
|
+
// 「已绑定并激活群消息助理」,所以本 CLI 够得着,不能当作不可达而不给提示。
|
|
98
|
+
"230002": "微信账号未绑定——群消息类接口要求先在 Gangtise 端绑定并激活群消息助理、且助理已入群。",
|
|
94
99
|
"240001": "换更早的 --period(如 2025q3 → 2025interim)。",
|
|
95
100
|
"240002": "改述后重新提交。",
|
|
96
101
|
"240003": "对照命令 --help 检查取值。",
|