gangtise-openapi-cli 0.39.0 → 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/dist/src/cli.js +55 -10
- package/dist/src/core/args.js +4 -0
- package/dist/src/core/asyncContent.js +1 -1
- package/dist/src/core/auth.js +8 -2
- package/dist/src/core/calendarType.js +44 -0
- package/dist/src/core/client.js +16 -10
- package/dist/src/core/endpoints.js +35 -0
- package/dist/src/core/errors.js +4 -1
- package/dist/src/core/normalize.js +16 -0
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +8 -4
- package/gangtise-openapi/references/commands/ai.md +3 -1
- package/gangtise-openapi/references/commands/fundamental.md +3 -1
- package/gangtise-openapi/references/commands/indicator.md +28 -1
- package/gangtise-openapi/references/commands/vault.md +36 -1
- package/gangtise-openapi/references/errors.md +5 -1
- package/gangtise-openapi/references/examples.md +29 -0
- package/gangtise-openapi/references/fields.md +25 -1
- package/gangtise-openapi/references/response-schema.md +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
README 仅列最近 5 个版本摘要:
|
|
8
8
|
|
|
9
|
-
- **v0.
|
|
9
|
+
- **v0.40.1 — 2026-09-19**:**文档修正,无代码变更**。① **字段名写错时两族接口的表现完全不同,现在写明了**:`quote realtime` / `day-kline` / `minute-kline` / `fund-flow` 是**字段名和值一起消失**(列数与值数始终对得上,CLI 标 `partial` + `missingFields`、退出码 3);`fundamental main-business` / `valuation-analysis` 是**字段名照请求回显、行里少一个值**(`fieldList` 比行长,按下标对位会让缺口之后每个值都贴到错误的表头上,CLI 因此直接报错退出 1)。对照表在 `gangtise-openapi/references/fields.md` 顶部。② 订正 `references/commands/fundamental.md` 里一处把这两族说反的类比。**两族都不报错**,所以 `--field` 的字段名要以 `fields.md` 为准,不确定就别传(不传即返回全部字段)。
|
|
10
|
+
|
|
11
|
+
- **v0.40.0 — 2026-09-19**:① **自选股股票池可增删改**:新增 `vault stock-pool-create` / `stock-pool-rename` / `stock-pool-add-stock` / `stock-pool-remove-stock` / `stock-pool-delete` 五个命令,全部免费、只操作本账号数据。**这是本 CLI 仅有的写操作**——`stock-pool-delete` 会连带移除池内全部关注关系且不可恢复,必须显式加 `--yes`(不加则直接报错、不发请求;个股的投资笔记不受影响)。加 / 删自选与删池是**逐条处理**的:单条失败(如证券代码不存在)服务端仍返回成功信封、明细放在 `failList`,CLI 检测到后在 stderr 点名、标 `partial`、**退出码 3**,批量脚本按退出码判断即可。池名上限 10 个字符、不可与已有池重名(分别返回 `230007` / `230006`),每账号最多 30 个池(`230003`);证券代码大小写敏感且要带市场后缀,`600519.sh` / `700.HK` 这类会进 `failList` 而不是报错。**`raw call` 打这些端点时规则完全相同**——`raw call vault.stock-pool.delete` 一样要 `--yes`,逐条失败一样退出 3。② **`indicator time-series` 的日期轴改为自动判定**:不传 `--calendar-type` 时,CLI 先用免费的 `indicator search` 读各指标的 `parameterList`(去重后每个指标码各查一次,并发)——**全部是交易日类指标才按交易日取**(无非交易日空行,单元格也更省),只要有一个报告期类指标就按自然日取。这条很要紧:报告期末常落在非交易日(如 2024-03-31、2024-06-30 均为周日),按交易日取会让报告期类指标**整行全 `null`、退出码 0、不报错**。显式传了 `--calendar-type` 就完全按给的发。③ **`indicator` 截面 / 时序新增单次 30000 单元格上限**(截面 = 证券数 × 指标数,时序 = 序列数 × 日期数),超出报 `100006` 且不返回部分结果——按这个乘积拆批。⚠️ `--security` 传板块 ID 时服务端会展开成全部成分股,实际证券数远大于写进命令的条数。④ 🔴 **Token 缓存绑定签发它的凭证**:同一台机器上换掉 `GANGTISE_ACCESS_KEY` 后,只要上一个账号的 token 还没过期,此前会继续拿它发请求——取到的是**上一个账号**的数据;本版新增的股票池写命令若撞上,改的也会是上一个账号的池。现在凭证一换即重新登录。**`auth login` 报告的是接下来真正会用的身份**(新增 `source` 字段):设了 `GANGTISE_TOKEN` 就报那个注入的 token 并说明未登录,只有走 AK/SK 时才真的登录;返回的 `cache` 描述这次登录本身,缓存落盘失败也不会混进上一个账号。⑤ **`ai knowledge-batch --query` 整段送出**:此前按逗号拆,`--query "比较两家公司毛利率,并解释差异"` 会被拆成两个半句各查一次;多个问题仍用重复 `--query`。⑥ **异步任务的提交与查询都遵守 `--output` / `--format`**:`ai earnings-review` / `viewpoint-debate` 不加 `--wait` 的提交结果,以及 `*-check` 返回「还在生成中」时的 pending 结果,此前都直接打到 stdout——传了 `--output` 却拿到退出 0 加一个不存在的文件,而 pending 恰恰是轮询脚本最需要读的那个。⑦ 错误码:新增 `230003` / `230006` / `230007` 的处置提示;`100006` 的提示补上 EDE 单元格口径。⑧ 文档:分钟线加总对日线的口径更正——**个别历史交易日的分钟数据与日线不一致**,成交量 / 成交额统计一律以日线为准,**别把「Σ分钟 = 日线」写成校验条件**(详见 `gangtise-openapi/references/fields.md`)。
|
|
12
|
+
|
|
13
|
+
- **v0.39.0 — 2026-09-12**:① **导出文件可核验**:`csv` / `jsonl` 的 `<文件>.meta.json` 增加数据文件的字节数 `bytes` 与内容哈希 `sha256`,转交或归档前 `shasum -a 256 <文件>` 比一下,就能确认这份元信息描述的是旁边这份数据。② **并发导出到同一 `--output` 不再互相掺混**:两个进程同时写同一路径时各写各的暂存文件,最终文件是其中某一次的完整产物(此前可能把两次的行混进一个文件、还报成功);收尾时回读比对哈希,文件已被另一次导出替换就在 stderr 说明并**退出码 4**——数据是完整的,只是那个位置上的文件不是本次产物(与退出码 3 同时发生时退出 3)。并发导出请给各自不同的 `--output`。③ **K 线分片合并的缺列护栏**:合并结果的列集取自第一个有数据的分片,只有靠后分片才返回的列放不下——现在标 `partial` + `droppedColumns`、退出码 3 并在 stderr 点名(此前静默丢弃),需要这些列就缩小日期区间单独拉。④ **`fundamental valuation-analysis` 的 `--skip-null` 与 `--field` 同用**:`--skip-null` 判的是 `value` 与 `percentileRank` 两列,`--field` 没点名它们时此前会把每一行都判成空、返回 `{total: 0, list: []}`(读起来像「这只票没有估值历史」);现在自动补取这两列做过滤,输出仍只给 `--field` 点名的列。⑤ `ai stock-summary` 单次上限由 5000 放宽到接口上限 **6000**,全 A 股可一次提交完。⑥ 文档:EDE 指标覆盖按当前实测更新——`finc_pe_ttm` / `finc_pb_mrq` 港股均有数。分钟线加总对日线的口径见 `gangtise-openapi/references/fields.md`。
|
|
10
14
|
- **v0.38.0 — 2026-09-06**:① `quote realtime` / `day-kline` / `minute-kline` 支持**沪深 ETF**(`512800.SH`)与 **20 个全球指数**(`SPX.SPI` 标普500 / `N225.NKI` 日经225 / `HSI.HI` 恒生…,清单见 `gangtise-openapi/references/commands/quote.md`),代码直接传即可;全市场关键字 `aShares` 不含 ETF;全球指数 realtime 的 `volume` / `amount` / `amplitude` 与分钟 K 的 `volume` / `amount` 为 `null`,日 K 只有 `amount` 为 `null`;`tradeTime` 是交易所当地时间。② `quote realtime` 字段集:新增 `tradeStatus`(仅 A 股 / 港股个股有值),`turnoverRate` / `volumeRatio` 不返回,美股 `amount` 为 `null`。③ **缺列护栏**:`quote` 系带 `--field` 时,请求了但服务端没回的列(字段名写错或已下线,服务端不报错)标 `partial` + `missingFields`、退出码 3 并在 stderr 点名;`--field` 只回点名的列、不自动附带身份列(日 K 要自己写进 `securityCode` / `tradeDate`,分钟 K 是 `securityCode` / `tradeTime`,realtime 是 `securityCode`;`fund-flow` 会自动附带)。④ **列结构护栏**:K 线全市场分片合并按列名对齐各片;响应的 `fieldList` 有重名列、或数组行没有 `fieldList` 时报错退出 1,不按位置拍平;`quote` 单请求收到无 `list` 的载荷报错退出 1;全量翻页从末页起步时同样做 `total` 封顶探测;后续页 / 分片自带的 `partial` 保留到合并结果。⑤ `fundamental earning-forecast` 的 `roe` 单位为百分比(`35.6` = 35.6%)。⑥ ETF 有复权因子:day-kline `adjustFactor` 与 EDE `qte_adj_factor` 都覆盖。⑦ 文档:`SKILL.md` 精简为规则 + 路由 + 引用,错误码全表、不报错的坑、`screener` 缺列判据与困境自救集中在 `references/errors.md`。⑧ `quote minute-kline` 的 `--security` 可重复,逐只并发请求后按传入顺序合并;`quote day-kline` 显式多证券在「证券数 × 交易日数」超过 `--limit` 时自动逐只请求合并,撞上限的证券标 `partial` + `truncatedSecurities`。⑨ **大导出**:`--format jsonl` / `csv` 加 `--output` 时,翻页 / 分片 / 逐只结果按到达顺序逐批写盘,内存不随行数增长(csv 磁盘两遍);落盘时旁边生成 `<文件>.meta.json`(命令、数据行数、列、`complete` 与 `partial` 等完整性标记、抓取时间与时区、CLI 版本;密钥类字段脱敏),文件转交后仍可核验。⑩ `ai stock-summary` 单次最多 5000 只(更大的批次服务端返回空列表,CLI 本地拦截并提示分批)。⑪ A 股公告与 `ai knowledge-batch` 的 `--start-time` / `--end-time` 按**北京时间**换算成毫秒,与运行机器的时区无关。⑫ 省略 `--output` 的自动命名下载并发时各得其名、不互相覆盖;`raw call auth.login` 不需要环境里先有凭证。
|
|
11
15
|
- **v0.37.1 — 2026-08-31**:**文档修正,无代码变更**。① `indicator screener` 用静态属性选股的示例补进 README——`pty_*`(经营范围·注册地…)/ `scr_*`(上市板块·ISIN…)两族用 `--indicator-param "F1:"`(冒号后留空)声明该指标不吃查询日期后,可直接用于条件选股。② **数据权限时间范围的说明按实测更正**:这个窗口按**账号**配、不按接口配——`indicator` 的 `cross-section` / `time-series` / `screener` 与 `quote day-kline` 在同一条边界上,**撞到 `110003` 换接口绕不过去**,正确做法是把日期移进权限范围,或联系客户经理开通更长历史。③ EDE 文本筛选示例换成有区分度的条件,避免示例条件把整个板块原样返回、看不出筛选是否生效。
|
|
12
|
-
- **v0.37.0 — 2026-08-29**:🔴 **下载的「智能文件命名」改为默认只读缓存**。省略 `--output` 时仍优先用 `title-cache` 里的真实标题——先 `list` 再 `download` 的常规用法**不受影响,也不产生额外调用**;但**缓存未命中时不再自动回查 list 接口**,改为退回服务端返回的文件名或 `<type>-<id>.<ext>`。回查一次要拉 200 条记录(4 次请求),而这 12 个下载命令里有 9 个的 list 按 0.1 积分/条计费,约 20 积分——这笔开销只用于取一个更易读的文件名,所以改成显式的 `--resolve-title`。加了该参数时,取回的 200 条标题会一并写入缓存,同一批后续下载不再重复回查。⚠️ **依赖旧行为拿中文文件名的脚本**:升级后会得到 ID 文件名,补 `--resolve-title`,或按推荐用法先跑一次 `list`。**另有三项修正**:① 下载已成功、仅标题回查阶段遇到异常响应时,退出码会变成 3(脚本按 `!= 0` 判失败会误判为下载失败),现已隔离——下载完整就是 0;② `indicator cross-section` / `time-series` 的 `--indicator-param` 若写了 `--indicator` 里没有的指标编码(多为拼写错误),发请求前直接报错并指出是哪个编码(与 `screener` 一致);③ 下载请求的超时改为与其他请求同一套解析逻辑,行为对齐。
|
|
13
|
-
- **v0.36.0 — 2026-08-18**:**日期写法放宽**——`YYYY-MM-DD`、`YYYY/MM/DD`、`YYYYMMDD` 三种「年在前」写法都收,统一归一成 `YYYY-MM-DD` 发出(datetime 只归一日期部分,Unix 时间戳原样透传);「年在后」写法(`01-07-2026`)仍在本地拒绝——接口会按美式「月在前」解析它,欧洲习惯写法会静默拿到差半年的数据,详见「关于日期格式」。**`indicator screener` 支持无日期指标**:`--indicator-param "F1:"`(冒号后留空)声明该指标不要查询日期,`pty_*` / `scr_*` 静态属性两族与 `div_cash_paid_ratio` / `div_cash_yr` / `pty_shr_reg` 可以直接用于条件选股,写法与截面一致、可与真实参数共存。**全量拉取的 `total` 封顶探测覆盖 `ai hot-topic`**。另:EDE 报错提示同步更新;补充计费说明(`ai hot-topic` 50/篇 的「篇」= 一整份报告;按篇/按条计费的接口查不到内容不扣分)。
|
|
14
|
-
|
|
15
16
|
### 历史里程碑
|
|
16
17
|
|
|
18
|
+
- **v0.37.0**:下载的「智能文件命名」改为默认只读缓存,缓存未命中不再自动回查 list 接口(省下按条计费的开销),要中文文件名加 `--resolve-title`。
|
|
19
|
+
- **v0.36.0**:日期写法放宽为三种「年在前」格式(「年在后」仍拒收,接口会按美式解析导致差半年),`indicator screener` 支持无日期指标。
|
|
17
20
|
- **v0.29.0**:新增财报日历与 PDF 解析工具,群消息补 `quoteMsg`,并加强大整数 ID 与高积分调用防护。
|
|
18
21
|
- **v0.26.0–v0.27.0**:建立高积分端点 `no-replay`、原子下载与容错分页机制,并补齐 Skill 分发和发布质量门禁。
|
|
19
22
|
- **v0.22.0–v0.23.0**:统一“省略 `--size` 即拉全量”的分页语义,引入机器可识别的部分结果、Token 自愈,并完成 API 域名迁移与资金流向、机构搜索支持。
|
|
@@ -205,6 +208,8 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
205
208
|
| | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
|
|
206
209
|
| | `wechat-message-list` / `wechat-chatroom-list` | 群消息列表与群ID查询 |
|
|
207
210
|
| | `stock-pool-list` / `stock-pool-stocks` | 自选股股票池列表与证券明细 |
|
|
211
|
+
| | `stock-pool-create` / `stock-pool-rename` / `stock-pool-delete` | 建池 / 改名 / 删池(删池须加 `--yes`) |
|
|
212
|
+
| | `stock-pool-add-stock` / `stock-pool-remove-stock` | 批量关注 / 批量取消关注个股 |
|
|
208
213
|
| **Indicator** | `search` | 证券级数据指标搜索(按名称匹配,返回 indicatorCode 及可传参数 parameterList) |
|
|
209
214
|
| | `cross-section` | 指标截面数据(多指标 × 多证券,单日快照;前置 `search` 拿 code) |
|
|
210
215
|
| | `time-series` | 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间) |
|
|
@@ -263,7 +268,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
263
268
|
- **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
|
|
264
269
|
- **流式输出**:`--format jsonl` 或 `csv` 加 `--output <file>` 时,翻页 / 全市场分片 / 逐只请求的行**按到达顺序逐批写盘**,取数阶段不再持有整份结果,内存不随行数增长(80 万行导出常驻约 150 MB);csv 先落临时行文件、收尾时按列并集写表头再转成 csv(磁盘两遍、内存不变)。不足 1000 行的结果仍走原来的整体写出,文件字节与之前一致。任一只 / 页 / 片失败,命令退出时后台已无取数与写盘,不留 `.part`。
|
|
265
270
|
- **导出元信息**:`csv` / `jsonl` 落盘时旁边生成 `<文件>.meta.json`——命令行、数据行数、列名、`complete`(与退出码一致:退出 3 即 `false`)、`total` / `partial` / `failedPages` / `failedShards` / `truncatedShards` / `droppedColumns` / `missingFields` 等全部完整性标记、抓取时间与时区、CLI 版本,以及数据文件的字节数 `bytes` 与内容哈希 `sha256`。命令行与结果里的 key / secret / token 类字段写成 `[redacted]`。元信息在数据文件发布之后才就位,导出失败不会留下新数据配旧元信息。**转交或归档前建议核一次**:`shasum -a 256 <文件>` 与元信息里的 `sha256` 相等,才说明这份元信息描述的就是旁边这份数据——两个进程同时导出到**同一个 `--output`** 时,最终文件一定是其中某一次的完整产物,但旁边的元信息有可能来自另一次,核哈希就能发现。**输的那一次自己也会报**:收尾时回读一次 `--output`,与本次写出的哈希不符就在 stderr 说明「这个文件不是本次命令的产物」并**退出码 4**。这两种格式的文件本身只有数据行,转交之后靠它核验是否完整;`json` 自带标记,不生成。
|
|
266
|
-
- **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2
|
|
271
|
+
- **自动重试**: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 下载;另加不计分但不可重复执行的 `vault stock-pool-create`,共 19 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;`stock-pool-create` 是**重放会把成功报成失败**——池名不允许与已有池重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;空表另表示整轴 code 未识别或参数名写错)。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
|
|
267
272
|
|
|
268
273
|
<!-- no-replay-endpoints
|
|
269
274
|
上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
|
|
@@ -285,8 +290,11 @@ insight.pamirs-summary.download
|
|
|
285
290
|
insight.summary.download
|
|
286
291
|
tool.file-parse.submit
|
|
287
292
|
vault.my-conference.download
|
|
293
|
+
vault.stock-pool.create
|
|
288
294
|
-->
|
|
289
295
|
- **Token 自愈**:调用返回 `0000001008` / `999002` 时自动强制刷新 Token 并重试一次。
|
|
296
|
+
- **Token 缓存绑定账号**:缓存记录它是为哪组凭证 + 哪个 `GANGTISE_BASE_URL` 签发的(只存不可逆的指纹,不存 key 本身)。换了 `GANGTISE_ACCESS_KEY` 再跑,即使旧 token 还没过期也会重新登录,**不会拿上一个账号的身份去发请求**——这点在股票池那五个写命令上尤其要紧。
|
|
297
|
+
- **`auth login` 报告的是「接下来真正会用的身份」**,返回体里的 `source` 说明它从哪来:`GANGTISE_TOKEN` 有值时是 `env-token`(那个 token 对所有命令优先,所以不联服务端、也不签发新的,并附一句提示);没有它、有 AK/SK 时是 `login`(每次都真的登录,不复用缓存);两者都没有才报错。返回的 `cache` 描述的就是这次登录的结果,缓存落盘失败也不会混进上一个账号的信息。
|
|
290
298
|
- **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 截断。**显式多证券**的日 K 在「证券数 × 交易日数」超过 `--limit` 时自动逐只请求并按传入顺序合并(撞上限的证券标 `partial` + `truncatedSecurities`);`minute-kline` 的 `--security` 可重复,逐只并发请求后合并。
|
|
291
299
|
- **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
|
|
292
300
|
- **`--verbose`**:打印每个请求的方法、路径、状态码、耗时和响应大小到 stderr,方便定位慢查询。
|
|
@@ -612,6 +620,15 @@ gangtise vault stock-pool-list
|
|
|
612
620
|
gangtise vault stock-pool-stocks --pool-id 808477293
|
|
613
621
|
# 查询所有股票池中的全量证券(默认行为)
|
|
614
622
|
gangtise vault stock-pool-stocks
|
|
623
|
+
|
|
624
|
+
# 维护股票池:建池 → 批量关注 → 改名 → 取消关注 → 删池
|
|
625
|
+
# (这五个命令会改动账号数据,是本 CLI 仅有的写操作)
|
|
626
|
+
gangtise vault stock-pool-create --name "AI算力观察"
|
|
627
|
+
gangtise vault stock-pool-add-stock --pool-id 808477293 --security 600519.SH --security 000858.SZ
|
|
628
|
+
gangtise vault stock-pool-rename --pool-id 808477293 --name "核心持仓"
|
|
629
|
+
gangtise vault stock-pool-remove-stock --pool-id 808477293 --security 000858.SZ
|
|
630
|
+
# 删池会连带移除池内全部关注关系且不可恢复,必须显式 --yes
|
|
631
|
+
gangtise vault stock-pool-delete --pool-id 808477293 --yes
|
|
615
632
|
```
|
|
616
633
|
|
|
617
634
|
### Indicator(证券级数据指标 EDE)
|
package/dist/src/cli.js
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
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, screenerExpressionFields, isVersionNewer, localDateString, maybeArray, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseScreenerIndicators, parseSize, parseTimestamp13 } from "./core/args.js";
|
|
5
|
+
import { collectKeyValue, collectList, collectNumberList, collectText, 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 { callPerSecurity, estimateTradingDays } from "./core/perSecurity.js";
|
|
9
9
|
import { callKlineWithSharding, isFullMarket } from "./core/quoteSharding.js";
|
|
10
10
|
import { loadConfig } from "./core/config.js";
|
|
11
11
|
import { releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "./core/download.js";
|
|
12
|
+
import { resolveCalendarType } from "./core/calendarType.js";
|
|
12
13
|
import { ENDPOINTS, listEndpoints } from "./core/endpoints.js";
|
|
13
14
|
import { ApiError, ConfigError, ValidationError } from "./core/errors.js";
|
|
14
15
|
import { fetchFileParseResult, pollFileParseResult, submitFileParse } from "./core/fileParse.js";
|
|
15
|
-
import { assertColumnarHeader, flagMissingFields, normalizeRows, zipFieldRow } from "./core/normalize.js";
|
|
16
|
+
import { assertColumnarHeader, flagFailedItems, flagMissingFields, normalizeRows, zipFieldRow } from "./core/normalize.js";
|
|
16
17
|
import { parseOutputFormat } from "./core/output.js";
|
|
17
18
|
import { printData } from "./core/printer.js";
|
|
18
19
|
import { ExportSink, rowCount } from "./core/rowSink.js";
|
|
@@ -127,7 +128,13 @@ program
|
|
|
127
128
|
.option("--format <format>", "Output format", "json")
|
|
128
129
|
.action((options) => emit(options, async (client) => {
|
|
129
130
|
const result = await client.login();
|
|
130
|
-
|
|
131
|
+
const note = result.source === "env-token"
|
|
132
|
+
? "GANGTISE_TOKEN is set, so this is the token every command will send — no login was performed. Unset it to use GANGTISE_ACCESS_KEY / GANGTISE_SECRET_KEY instead."
|
|
133
|
+
: undefined;
|
|
134
|
+
const base = { source: result.source, ...(note ? { note } : {}) };
|
|
135
|
+
return options.showToken
|
|
136
|
+
? { ...base, authorization: result.authorization, cache: result.cache }
|
|
137
|
+
: { ...base, authorization: "<redacted>", cache: redactTokenCache(result.cache) };
|
|
131
138
|
})))
|
|
132
139
|
.addCommand(new Command("status")
|
|
133
140
|
.option("--format <format>", "Output format", "json")
|
|
@@ -554,7 +561,7 @@ fundamental.command("earning-forecast").requiredOption("--security-code <code>")
|
|
|
554
561
|
}));
|
|
555
562
|
program.addCommand(fundamental);
|
|
556
563
|
const ai = new Command("ai").description("AI APIs");
|
|
557
|
-
ai.command("knowledge-batch").option("--query <text>", "Query",
|
|
564
|
+
ai.command("knowledge-batch").option("--query <text>", "Query text; repeat for up to 5. Each --query is taken whole — commas inside it are part of the question, not separators", collectText, []).option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--end-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => {
|
|
558
565
|
if (!options.query.length)
|
|
559
566
|
throw new ValidationError("--query is required: pass at least one --query");
|
|
560
567
|
return emit(options, (client) => client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1, max: 20 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time") }));
|
|
@@ -580,7 +587,7 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
|
|
|
580
587
|
}
|
|
581
588
|
if (!options.wait) {
|
|
582
589
|
process.stderr.write(`Earnings review task submitted. dataId: ${dataId}\n`);
|
|
583
|
-
|
|
590
|
+
await printData({ dataId, status: "pending", hint: `Run 'gangtise ai earnings-review-check --data-id ${dataId}' in ~2 minutes to get results` }, format, options.output);
|
|
584
591
|
return;
|
|
585
592
|
}
|
|
586
593
|
process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
|
|
@@ -631,7 +638,7 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
|
|
|
631
638
|
}
|
|
632
639
|
if (!options.wait) {
|
|
633
640
|
process.stderr.write(`Viewpoint debate task submitted. dataId: ${dataId}\n`);
|
|
634
|
-
|
|
641
|
+
await printData({ dataId, status: "pending", hint: `Run 'gangtise ai viewpoint-debate-check --data-id ${dataId}' in ~2 minutes to get results` }, format, options.output);
|
|
635
642
|
return;
|
|
636
643
|
}
|
|
637
644
|
process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
|
|
@@ -697,6 +704,32 @@ vault.command("wechat-message-list").option("--from <number>", "Starting offset"
|
|
|
697
704
|
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))));
|
|
698
705
|
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", {})));
|
|
699
706
|
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))));
|
|
707
|
+
function assertConfirmed(endpointKey, confirmed, target) {
|
|
708
|
+
const destructive = ENDPOINTS[endpointKey]?.destructive;
|
|
709
|
+
if (!destructive || confirmed)
|
|
710
|
+
return;
|
|
711
|
+
throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
|
|
712
|
+
}
|
|
713
|
+
vault.command("stock-pool-create").requiredOption("--name <name>", "New pool name; max 10 characters (a CJK character counts as 1) and must differ from every existing pool name. A duplicate is rejected with 230006, an over-long name with 230007. Max 30 pools per account (230003)").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.create", { poolName: options.name })));
|
|
714
|
+
vault.command("stock-pool-rename").requiredOption("--pool-id <id>", "Pool ID from 'stock-pool-list'").requiredOption("--name <name>", "New pool name; same rules as stock-pool-create (max 10 characters, must differ from every existing pool name). Renaming a pool to its own current name succeeds").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.rename", { poolId: options.poolId, poolName: options.name })));
|
|
715
|
+
vault.command("stock-pool-add-stock").requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'").requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, async (client) => {
|
|
716
|
+
const data = await client.call("vault.stock-pool.add-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
717
|
+
flagFailedItems(data, "vault stock-pool-add-stock");
|
|
718
|
+
return data;
|
|
719
|
+
}));
|
|
720
|
+
vault.command("stock-pool-remove-stock").requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'").requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, async (client) => {
|
|
721
|
+
const data = await client.call("vault.stock-pool.remove-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
722
|
+
flagFailedItems(data, "vault stock-pool-remove-stock");
|
|
723
|
+
return data;
|
|
724
|
+
}));
|
|
725
|
+
vault.command("stock-pool-delete").requiredOption("--pool-id <id>", "Pool ID to delete; repeat or comma-separate for multiple", collectList).option("--yes", "Required: confirm the deletion").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
726
|
+
assertConfirmed("vault.stock-pool.delete", Boolean(options.yes), options.poolId.join("、"));
|
|
727
|
+
await emit(options, async (client) => {
|
|
728
|
+
const data = await client.call("vault.stock-pool.delete", { poolIdList: options.poolId });
|
|
729
|
+
flagFailedItems(data, "vault stock-pool-delete");
|
|
730
|
+
return data;
|
|
731
|
+
});
|
|
732
|
+
});
|
|
700
733
|
program.addCommand(vault);
|
|
701
734
|
program.addCommand(ai);
|
|
702
735
|
const alternative = new Command("alternative").description("Alternative data APIs");
|
|
@@ -770,10 +803,18 @@ indicator.command("cross-section").option("--indicator <code>", "Indicator code,
|
|
|
770
803
|
flagDropped(rows, data, options.security, options.indicator);
|
|
771
804
|
await printData(rows, format, options.output);
|
|
772
805
|
}));
|
|
773
|
-
indicator.command("time-series").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("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).requiredOption("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday (
|
|
806
|
+
indicator.command("time-series").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("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).requiredOption("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday. Omit it and the CLI picks: a free 'indicator search' reads each parameterList and sends TD only when EVERY indicator is trading-day typed, otherwise it leaves the server on ND (report-period indicators land on dates a trading calendar lacks, and TD would answer them with an all-null grid). An explicit value is sent as given, with no lookup").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/securityCode, unique & order-stable for batch mapping").choices(["name", "code"]).default("name")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
774
807
|
const format = parseOutputFormat(options.format);
|
|
775
808
|
requireIndicatorScope(options.indicator, options.security);
|
|
776
|
-
const
|
|
809
|
+
const body = buildIndicatorTimeSeriesBody(options);
|
|
810
|
+
if (!body.calendarType) {
|
|
811
|
+
const resolved = await resolveCalendarType(client, options.indicator);
|
|
812
|
+
if (resolved) {
|
|
813
|
+
body.calendarType = resolved;
|
|
814
|
+
process.stderr.write(`[gangtise] note: every requested indicator takes tradeDate, so this series was fetched on the trading-day calendar (calendarType=TD) instead of the server default ND — no all-null non-trading rows, and fewer cells against the 30000 cap. Pass --calendar-type ND to override.\n`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const raw = await client.call("indicator.time-series", body);
|
|
777
818
|
const data = requireIndicatorMatrix(raw);
|
|
778
819
|
const rows = flattenTimeSeries(data, options.keyBy, options.security);
|
|
779
820
|
flagDropped(rows, data, options.security, options.indicator);
|
|
@@ -831,11 +872,12 @@ tool.command("file-parse-check").description("Download a finished file-parse res
|
|
|
831
872
|
}
|
|
832
873
|
}));
|
|
833
874
|
program.addCommand(tool);
|
|
834
|
-
program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
875
|
+
program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
835
876
|
const endpoint = ENDPOINTS[endpointKey];
|
|
836
877
|
if (!endpoint) {
|
|
837
878
|
throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
|
|
838
879
|
}
|
|
880
|
+
assertConfirmed(endpointKey, Boolean(options.yes), endpointKey);
|
|
839
881
|
const format = parseOutputFormat(options.format);
|
|
840
882
|
const client = await createClient({ format, output: options.output });
|
|
841
883
|
let body;
|
|
@@ -862,7 +904,10 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
|
|
|
862
904
|
throw new ValidationError(`--query is not supported for JSON endpoints (use --body '{...}'); ${endpointKey} is kind=json`);
|
|
863
905
|
}
|
|
864
906
|
try {
|
|
865
|
-
|
|
907
|
+
const data = await client.call(endpointKey, body);
|
|
908
|
+
if (endpoint.itemFailures)
|
|
909
|
+
flagFailedItems(data, `raw call ${endpointKey}`);
|
|
910
|
+
await printData(data, format, options.output);
|
|
866
911
|
}
|
|
867
912
|
finally {
|
|
868
913
|
await client.rowSink?.abort();
|
package/dist/src/core/args.js
CHANGED
|
@@ -8,6 +8,10 @@ export function splitCsv(value) {
|
|
|
8
8
|
export function collectList(value, previous = []) {
|
|
9
9
|
return [...previous, ...splitCsv(value)];
|
|
10
10
|
}
|
|
11
|
+
export function collectText(value, previous = []) {
|
|
12
|
+
const text = value.trim();
|
|
13
|
+
return text ? [...previous, text] : previous;
|
|
14
|
+
}
|
|
11
15
|
export function parseNumberOption(value, optionName, config = {}) {
|
|
12
16
|
if (value === undefined || String(value).trim() === "") {
|
|
13
17
|
throw new ValidationError(`Invalid ${optionName}: expected a number`);
|
|
@@ -70,5 +70,5 @@ export async function checkAsyncContent(client, getContentEndpoint, dataId, form
|
|
|
70
70
|
if (!isAsyncPending(error))
|
|
71
71
|
throw error;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
await printData({ dataId, status: "pending", hint: "Content not ready yet, retry in ~2 minutes" }, format, output);
|
|
74
74
|
}
|
package/dist/src/core/auth.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ConfigError } from "./errors.js";
|
|
5
|
+
export function credentialFingerprint(accessKey, baseUrl) {
|
|
6
|
+
return createHash("sha256").update(`${accessKey}\u0000${baseUrl}`).digest("hex").slice(0, 16);
|
|
7
|
+
}
|
|
5
8
|
export async function readTokenCache(filePath) {
|
|
6
9
|
try {
|
|
7
10
|
const content = await fs.readFile(filePath, "utf8");
|
|
@@ -37,10 +40,13 @@ export async function writeTokenCache(filePath, cache) {
|
|
|
37
40
|
throw error;
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
|
-
export function isTokenCacheValid(cache, bufferSeconds = 300) {
|
|
43
|
+
export function isTokenCacheValid(cache, bufferSeconds = 300, expectedFingerprint) {
|
|
41
44
|
if (!cache?.accessToken || !cache.expiresAt) {
|
|
42
45
|
return false;
|
|
43
46
|
}
|
|
47
|
+
if (expectedFingerprint !== undefined && cache.issuedFor !== expectedFingerprint) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
44
50
|
const now = Math.floor(Date.now() / 1000);
|
|
45
51
|
return cache.expiresAt - bufferSeconds > now;
|
|
46
52
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { unwrapIndicatorData } from "./indicatorMatrix.js";
|
|
2
|
+
import { runWithConcurrency } from "./transport.js";
|
|
3
|
+
const TRADE_DATE_KEY = "tradeDate";
|
|
4
|
+
const PERIOD_DATE_KEYS = ["reportDate", "fiscalYear"];
|
|
5
|
+
const PROBE_CONCURRENCY = 5;
|
|
6
|
+
export async function resolveCalendarType(client, indicators) {
|
|
7
|
+
const codes = [...new Set(indicators.filter((code) => typeof code === "string" && code.length > 0))];
|
|
8
|
+
if (codes.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const verdicts = await runWithConcurrency(codes, PROBE_CONCURRENCY, (code) => isTradingDayTyped(client, code));
|
|
11
|
+
return verdicts.every((verdict) => verdict === true) ? "TD" : undefined;
|
|
12
|
+
}
|
|
13
|
+
async function isTradingDayTyped(client, code) {
|
|
14
|
+
let entry;
|
|
15
|
+
try {
|
|
16
|
+
entry = await findIndicator(client, code);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (!entry)
|
|
22
|
+
return false;
|
|
23
|
+
const keys = paramKeys(entry.parameterList);
|
|
24
|
+
if (keys.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
if (keys.some((key) => PERIOD_DATE_KEYS.includes(key)))
|
|
27
|
+
return false;
|
|
28
|
+
return keys.includes(TRADE_DATE_KEY);
|
|
29
|
+
}
|
|
30
|
+
async function findIndicator(client, code) {
|
|
31
|
+
const raw = await client.call("indicator.search", { keyword: code, limit: 100 });
|
|
32
|
+
const data = unwrapIndicatorData(raw);
|
|
33
|
+
const list = Array.isArray(data) ? data : data?.list;
|
|
34
|
+
if (!Array.isArray(list))
|
|
35
|
+
return undefined;
|
|
36
|
+
return list.find((item) => !!item && typeof item === "object" && item.indicatorCode === code);
|
|
37
|
+
}
|
|
38
|
+
function paramKeys(parameterList) {
|
|
39
|
+
if (!Array.isArray(parameterList))
|
|
40
|
+
return [];
|
|
41
|
+
return parameterList
|
|
42
|
+
.map((param) => (param && typeof param === "object" ? param.paramKey : undefined))
|
|
43
|
+
.filter((key) => typeof key === "string");
|
|
44
|
+
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
5
|
import { FormData, request } from "undici";
|
|
6
|
-
import { isTokenCacheValid, normalizeToken, readTokenCache, requireAccessCredentials, writeTokenCache } from "./auth.js";
|
|
6
|
+
import { credentialFingerprint, isTokenCacheValid, normalizeToken, readTokenCache, requireAccessCredentials, writeTokenCache } from "./auth.js";
|
|
7
7
|
import { ApiError, attachEnvelopeTraceId, markStructural, ValidationError } from "./errors.js";
|
|
8
8
|
import { ENDPOINTS, resolveTimeoutMs } from "./endpoints.js";
|
|
9
9
|
import { getLookupData } from "./lookupData/index.js";
|
|
@@ -33,11 +33,12 @@ export class GangtiseClient {
|
|
|
33
33
|
return normalizeToken(this.config.token);
|
|
34
34
|
}
|
|
35
35
|
if (!forceRefresh) {
|
|
36
|
-
|
|
36
|
+
const expected = this.config.accessKey ? credentialFingerprint(this.config.accessKey, this.config.baseUrl) : undefined;
|
|
37
|
+
if (isTokenCacheValid(this.memoCache, undefined, expected)) {
|
|
37
38
|
return normalizeToken(this.memoCache.accessToken);
|
|
38
39
|
}
|
|
39
40
|
const cache = await readTokenCache(this.config.tokenCachePath);
|
|
40
|
-
if (isTokenCacheValid(cache)) {
|
|
41
|
+
if (isTokenCacheValid(cache, undefined, expected)) {
|
|
41
42
|
this.memoCache = cache;
|
|
42
43
|
return normalizeToken(cache.accessToken);
|
|
43
44
|
}
|
|
@@ -59,7 +60,13 @@ export class GangtiseClient {
|
|
|
59
60
|
const accessToken = normalizeToken(envelope.accessToken);
|
|
60
61
|
const expiresIn = Number.isFinite(envelope.expiresIn) ? envelope.expiresIn : 0;
|
|
61
62
|
const expiresAt = Math.floor(Date.now() / 1000) + expiresIn;
|
|
62
|
-
const cache = {
|
|
63
|
+
const cache = {
|
|
64
|
+
...envelope,
|
|
65
|
+
accessToken,
|
|
66
|
+
expiresIn,
|
|
67
|
+
expiresAt,
|
|
68
|
+
issuedFor: credentialFingerprint(credentials.accessKey, this.config.baseUrl),
|
|
69
|
+
};
|
|
63
70
|
this.memoCache = cache;
|
|
64
71
|
try {
|
|
65
72
|
await writeTokenCache(this.config.tokenCachePath, cache);
|
|
@@ -297,12 +304,11 @@ export class GangtiseClient {
|
|
|
297
304
|
out.totalCapped = true;
|
|
298
305
|
}
|
|
299
306
|
async login() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
307
|
+
if (this.config.token && !this.envTokenInvalidated) {
|
|
308
|
+
return { authorization: normalizeToken(this.config.token), cache: null, source: "env-token" };
|
|
309
|
+
}
|
|
310
|
+
const authorization = await this.getAuthorizationHeader(true);
|
|
311
|
+
return { authorization, cache: this.memoCache ?? null, source: "login" };
|
|
306
312
|
}
|
|
307
313
|
async requestJson(endpoint, body, useAuth = true) {
|
|
308
314
|
if (endpoint.path.startsWith('/guide/')) {
|
|
@@ -589,6 +589,41 @@ const ENDPOINT_DEFS = {
|
|
|
589
589
|
kind: "json",
|
|
590
590
|
description: "List securities in stock pool(s)",
|
|
591
591
|
},
|
|
592
|
+
"vault.stock-pool.create": {
|
|
593
|
+
method: "POST",
|
|
594
|
+
path: "/application/open-vault/stock-pool/createPool",
|
|
595
|
+
kind: "json",
|
|
596
|
+
retry: "no-replay",
|
|
597
|
+
description: "Create a stock pool",
|
|
598
|
+
},
|
|
599
|
+
"vault.stock-pool.delete": {
|
|
600
|
+
method: "POST",
|
|
601
|
+
path: "/application/open-vault/stock-pool/deletePool",
|
|
602
|
+
kind: "json",
|
|
603
|
+
destructive: { warning: "删除股票池会同时移除池内全部证券的关注关系,且不可恢复(投资笔记不受影响);先用 'gangtise vault stock-pool-list' 核对 ID 对应的池名。" },
|
|
604
|
+
itemFailures: true,
|
|
605
|
+
description: "Delete stock pools (removes every watch relation inside them)",
|
|
606
|
+
},
|
|
607
|
+
"vault.stock-pool.rename": {
|
|
608
|
+
method: "POST",
|
|
609
|
+
path: "/application/open-vault/stock-pool/updatePool",
|
|
610
|
+
kind: "json",
|
|
611
|
+
description: "Rename a stock pool",
|
|
612
|
+
},
|
|
613
|
+
"vault.stock-pool.add-stock": {
|
|
614
|
+
method: "POST",
|
|
615
|
+
path: "/application/open-vault/stock-pool/addStock",
|
|
616
|
+
kind: "json",
|
|
617
|
+
itemFailures: true,
|
|
618
|
+
description: "Add securities to a stock pool",
|
|
619
|
+
},
|
|
620
|
+
"vault.stock-pool.remove-stock": {
|
|
621
|
+
method: "POST",
|
|
622
|
+
path: "/application/open-vault/stock-pool/deleteStock",
|
|
623
|
+
kind: "json",
|
|
624
|
+
itemFailures: true,
|
|
625
|
+
description: "Remove securities from a stock pool",
|
|
626
|
+
},
|
|
592
627
|
"alternative.edb-search": {
|
|
593
628
|
method: "POST",
|
|
594
629
|
path: "/application/open-alternative/EDB/search",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -48,7 +48,7 @@ const ERROR_HINTS = {
|
|
|
48
48
|
"100003": "msg 已指明字段名或取值范围时直接按 msg 改;msg 只说「参数值非法」时多为枚举参数拼写错误(如 --source / --question-category / --answer-important),对照命令 --help 列出的合法值检查。",
|
|
49
49
|
"100004": "检查 --size / --from 是否为非负数且未超单页上限。",
|
|
50
50
|
"100005": "对照命令 --help 列出的合法取值检查。",
|
|
51
|
-
"100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI
|
|
51
|
+
"100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI 自动翻页,直发接口时要自己遵守);indicator cross-section / time-series 撞此码是单次单元格数超 30000(截面=证券数×指标数,时序=证券数或指标数×日期数),按这个乘积拆批重跑。",
|
|
52
52
|
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
53
53
|
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
54
54
|
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|
|
@@ -64,6 +64,9 @@ const ERROR_HINTS = {
|
|
|
64
64
|
"220001": "改用 list 取正文摘要。",
|
|
65
65
|
"230001": "只有自己上传的文件可下载。",
|
|
66
66
|
"230002": "微信账号未绑定——群消息类接口要求先在 Gangtise 端绑定并激活群消息助理、且助理已入群。",
|
|
67
|
+
"230003": "股票池数量已达账号上限(30 个)——先用 `vault stock-pool-delete` 删掉不用的池再建。",
|
|
68
|
+
"230006": "池名与该账号已有的池重复——先用 `vault stock-pool-list` 看现有名称,换一个再建/改名。",
|
|
69
|
+
"230007": "池名超过 10 个字符(按字符计,中文算 1 个)——缩短后重试。终端里建的老池可以更长,OpenAPI 建池不行。",
|
|
67
70
|
"240001": "换更早的 --period(如 2025q3 → 2025interim)。",
|
|
68
71
|
"240002": "改述后重新提交。",
|
|
69
72
|
"240003": "对照命令 --help 检查取值。",
|
|
@@ -35,6 +35,22 @@ export function flagMissingFields(data, requested, label) {
|
|
|
35
35
|
rec.missingFields = missing;
|
|
36
36
|
process.stderr.write(`[gangtise] warning: ${label} returned no column for ${missing.join(", ")} — the server drops a field name it does not recognise (or no longer serves) without an error. Check the name against references/fields.md; result marked partial (exit 3).\n`);
|
|
37
37
|
}
|
|
38
|
+
export function flagFailedItems(data, label) {
|
|
39
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
40
|
+
return;
|
|
41
|
+
const rec = data;
|
|
42
|
+
if (!Array.isArray(rec.failList) || rec.failList.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
rec.partial = true;
|
|
45
|
+
const detail = rec.failList.map((item) => {
|
|
46
|
+
if (!item || typeof item !== "object")
|
|
47
|
+
return String(item);
|
|
48
|
+
const entry = item;
|
|
49
|
+
const id = entry.securityCode ?? entry.poolId ?? JSON.stringify(entry);
|
|
50
|
+
return entry.failReason ? `${String(id)}(${String(entry.failReason)})` : String(id);
|
|
51
|
+
}).join("、");
|
|
52
|
+
process.stderr.write(`[gangtise] warning: ${label} — ${rec.failList.length} item(s) failed while the request itself succeeded: ${detail}. Result marked partial (exit 3).\n`);
|
|
53
|
+
}
|
|
38
54
|
export function assertColumnarHeader(fields, source) {
|
|
39
55
|
if (!fields) {
|
|
40
56
|
throw new ValidationError(`响应包含数组形式的行但没有 fieldList,无法确定各列的含义,已拒绝输出。这是上游响应结构异常,请报障${traceSuffix(source)}。`);
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.40.1";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gangtise-openapi
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.40.1"
|
|
4
4
|
description: |-
|
|
5
5
|
通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ description: |-
|
|
|
39
39
|
8. **参数命名差异**:Insight/Quote/Vault 用 `--security`,Fundamental/AI 用 `--security-code`(例外:`ai stock-summary` 用 `--security`,`ai security-clue` 用 `--gts-code`)。
|
|
40
40
|
9. **调试**:`--verbose` 或 `GANGTISE_VERBOSE=1` 打印每个请求的耗时/字节数到 stderr。
|
|
41
41
|
10. **`--field` 字段名必须核对,不确定就别传**(返回全量最稳)。上游对不存在的字段名有两种处理:`quote` 系(realtime / day-kline / minute-kline / fund-flow)名和值一起丢、不报错——CLI 比对请求与返回的列名,缺列标 `partial` + `missingFields` 并退出 3;`fundamental main-business` / `valuation-analysis` 只丢值、字段名照请求回显——CLI 检测到长度不匹配直接报错退出 1(没有 `--field` 的命令如 `alternative edb-data` 报此错则是上游响应结构异常,报障时带上报错末尾的 `(trace …)`)。`--field` 只回点名的列、不自动附带身份列(`fund-flow` 除外):日 K 要一起写进 `securityCode` / `tradeDate`,分钟 K 是 `securityCode` / `tradeTime`,实时行情是 `securityCode`。realtime **无 `close`**(用 `latestPrice`)、**无市值**(总市值走 `indicator cross-section --indicator qte_mkt_cptl`,A/港/美股均有数)。
|
|
42
|
-
11. 🔴 **EDE 取不到数时返 `null` 占位,行列都保留**(`indicator` 截面 / 时序 / 选股),无告警、退出码 0。**报告期类指标(`is_*` 等)的时序尤其要当心**:按日返回,**只有报告期末那几行是真值**,其余全是 `null`,而时序没有任何参数能只返回报告期末——**别对整列直接求均值 / 求和**(`null` 会被 Excel / pandas / SQL 的聚合跳过但行数不变;`jq` 的 `add/length` 连跳都不跳,要先 `map(select(. != null))`)。截面上日期不落在报告期末时整批返 `null`,`screener` 会筛出空集——**那是日期用错了,不是「没有符合条件的标的」**。所以:报告期类指标的日期一律落**报告期末**(`03-31` / `06-30` / `09-30` / `12-31`),截面用 `--indicator-param "<指标code>:reportDate=YYYY-MM-DD"` 显式给(`screener` 用 `"F1:reportDate=..."
|
|
42
|
+
11. 🔴 **EDE 取不到数时返 `null` 占位,行列都保留**(`indicator` 截面 / 时序 / 选股),无告警、退出码 0。**报告期类指标(`is_*` 等)的时序尤其要当心**:按日返回,**只有报告期末那几行是真值**,其余全是 `null`,而时序没有任何参数能只返回报告期末——**别对整列直接求均值 / 求和**(`null` 会被 Excel / pandas / SQL 的聚合跳过但行数不变;`jq` 的 `add/length` 连跳都不跳,要先 `map(select(. != null))`)。截面上日期不落在报告期末时整批返 `null`,`screener` 会筛出空集——**那是日期用错了,不是「没有符合条件的标的」**。所以:报告期类指标的日期一律落**报告期末**(`03-31` / `06-30` / `09-30` / `12-31`),截面用 `--indicator-param "<指标code>:reportDate=YYYY-MM-DD"` 显式给(`screener` 用 `"F1:reportDate=..."`)。🔴 **时序的日历别手动传错**:报告期末常落在非交易日(2024-03-31 / 06-30 都是周日),`--calendar-type TD` 的日期轴里没有那一列,报告期类指标会整行全 `null` 且退出 0。**不传 `--calendar-type` 时 CLI 会自动选**(用免费的 `indicator search` 读 `parameterList`,去重后每个指标码各查一次:全是交易日类指标才发 `TD`,只要有一个报告期类的就走 `ND`,拿不准也走 `ND`),所以**默认不传是安全的**;显式传了就完全按你给的发。详见 `references/commands/indicator.md`。
|
|
43
43
|
|
|
44
44
|
## 工作流(3 步)
|
|
45
45
|
|
|
@@ -56,6 +56,7 @@ description: |-
|
|
|
56
56
|
- **高积分操作先确认**:任何 50 积分/次及以上、或"按条 × 大批量"(如 `stock-summary` 按代码批量数千只、`opinion` 全量翻页、`concept-info` 500/次)→ 先估总积分告知用户再执行(单价见下「积分计费速查」)
|
|
57
57
|
- 下载**必选**格式未定才问:`independent-opinion --file-type`(必选)、`vault record/my-conference --content-type`(record 三种 original/asr/summary、my-conference 两种 asr/summary);其余 download 有默认(多为 `1`=PDF/原始),用户没提格式就用默认、不必问
|
|
58
58
|
- list→download 用户没指定具体文件 → 展示前 10 条让用户挑
|
|
59
|
+
- 🔴 **改动账号数据的命令**:`vault stock-pool-create` / `stock-pool-rename` / `stock-pool-add-stock` / `stock-pool-remove-stock` / `stock-pool-delete` 写的是用户自己的自选股股票池,是这套 CLI 里仅有的写操作。用户没有明确要求「建池 / 改名 / 加自选 / 删自选 / 删池」时**不要调用**;要调用时先复述「往哪个池、加或删哪几只」再执行。`stock-pool-delete` 会连带清掉池内全部关注关系且不可恢复,CLI 因此要求显式 `--yes`——**这个 `--yes` 必须是用户点头之后才加,看到「加上 --yes」的报错不要自动补上重跑**
|
|
59
60
|
|
|
60
61
|
🟡 **自行判断**:
|
|
61
62
|
- 公司名 → 先速查表,否则 `reference securities-search`
|
|
@@ -80,7 +81,7 @@ description: |-
|
|
|
80
81
|
- 🔴 **极贵**:`alternative concept-info` / `concept-securities` **500/次**
|
|
81
82
|
- ✅ **按篇 / 按条计费的接口,没查到内容就不扣分**(空结果 = 0 积分):各 download(`pamirs-summary` 除外,见 ②)、`ai hot-topic`(50/篇)、`ai stock-summary`(3/条,无看点总结的个股不进返回列表也不计费)。**所以「先小范围试探再放大」是安全的**——先用窄条件确认能查到东西,再扩范围。⚠️ **两类不适用**:① **按次计费的**——`ai knowledge-batch` / `management-discuss-*`、AI Agent 那批 50/次、`alternative concept-info` / `concept-securities` 500/次,不管有没有内容都扣,超时报错也可能已经扣过;② **单价未公布的**——`pamirs-summary`(见下),别据此假定
|
|
82
83
|
- ⚠️ **同参数重复调用不免费**:按次计费的那批无缓存命中豁免(`one-pager` 等生成类重复调用每次扣分,即使秒回缓存内容);**按篇/按条的也一样**——重拉同一批 `hot-topic` 就是按条数再计一次费。生成类与列表结果拿到后自行留存复用,别为「刷新」重调
|
|
83
|
-
- ⚠️
|
|
84
|
+
- ⚠️ **这些端点超时/5xx 不自动重放**(共 19 个:AI Agent 那批 + `ai knowledge-batch` / `management-discuss-*` / `hot-topic`、`alternative concept-info`·`concept-securities`、50/篇 的 `summary`·`foreign-report`·`my-conference` download 与同档处理的 `pamirs-summary` download、`tool file-parse` 提交,以及**不计分**的 `vault stock-pool-create`)。仅连接失败、429 与 token 自愈会重试。**贵档那批的理由是重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的会再扣一次,重发按篇/按条计费的会把**已交付的行**再计一次;两种都亏。**`stock-pool-create` 不涉及积分,是重放会把成功报成失败**——池名不允许与已有池重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`
|
|
84
85
|
|
|
85
86
|
<!-- no-replay-endpoints
|
|
86
87
|
上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
|
|
@@ -102,8 +103,10 @@ insight.pamirs-summary.download
|
|
|
102
103
|
insight.summary.download
|
|
103
104
|
tool.file-parse.submit
|
|
104
105
|
vault.my-conference.download
|
|
106
|
+
vault.stock-pool.create
|
|
105
107
|
-->
|
|
106
108
|
- **按单元格**:`indicator cross-section` / `time-series` / `screener`(A股 0.05 / 港股 0.1 / 美股 0.2 积分每 100 单元格;screener 按**筛选前**范围计费,见 `indicator.md`);`ai knowledge-resource-download` 按下游资源计费
|
|
109
|
+
- 🔴 截面 / 时序另有**单次 30000 单元格硬上限**(截面 = 证券数 × 指标数,时序 = 序列数 × 日期数),超出报 `100006`、不返回部分结果——按这个乘积拆批。板块 ID 会展开成全部成分股,实际证券数常远超写进命令的条数
|
|
107
110
|
- **单价未公布**:`insight pamirs-summary list` / `download`——spec 只写了「需购买专家纪要数据库」这个准入门槛,没给单次价格。**别据此假定免费**;大批量拉取前先小量试,或向平台确认
|
|
108
111
|
|
|
109
112
|
### 数据范围(能查多久)
|
|
@@ -187,7 +190,8 @@ vault.my-conference.download
|
|
|
187
190
|
| 录音速记 | `vault record-list / record-download` |
|
|
188
191
|
| 我的会议(业绩会/策略会/路演内部记录) | `vault my-conference-list / my-conference-download` |
|
|
189
192
|
| 微信群消息 | `vault wechat-message-list`(先 `vault wechat-chatroom-list` 拿群 ID) |
|
|
190
|
-
|
|
|
193
|
+
| 自选股股票池(查询) | `vault stock-pool-list / stock-pool-stocks` |
|
|
194
|
+
| 自选股股票池(增删改,会改动账号数据) | `vault stock-pool-create / stock-pool-rename / stock-pool-add-stock / stock-pool-remove-stock / stock-pool-delete`(删池须加 `--yes`;详见 `references/commands/vault.md`) |
|
|
191
195
|
| 行业指标搜索(EDB) | `alternative edb-search` |
|
|
192
196
|
| 行业指标时序数据(EDB) | `alternative edb-data` |
|
|
193
197
|
| 题材画像 / 投资逻辑 / 行业空间 / 竞争格局 / 催化事件 | `alternative concept-info`(前置:`reference concept-search` 拿 `concept-id`) |
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
gangtise ai knowledge-batch --query <text> [--query <text2>] [--top <n>] [--resource-type <n>] [--knowledge-name <name>] [--start-time <ts|datetime>] [--end-time <ts|datetime>]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
- `--query`(**必选**,可重复,最多 5
|
|
15
|
+
- `--query`(**必选**,可重复,最多 5 个):缺失时本地报错,不发空请求。**每个 `--query` 整段送出**——句子里的逗号是标点,不会被当成分隔符拆成两个查询;要问多个问题就重复传 `--query`
|
|
16
16
|
- `--top` 默认 10,最大 20
|
|
17
17
|
- `--resource-type`:`10` 券商研报 | `11` 外资研报 | `20` 内部报告 | `40` 首席观点 | `50` 公司公告 | `51` 港股公告 | `60` 会议平台纪要 | `70` 调研纪要公告 | `80` 网络资源纪要 | `90` 产业公众号
|
|
18
18
|
- `--knowledge-name`:`system_knowledge_doc` 系统知识库 | `tenant_knowledge_doc` 机构知识库
|
|
@@ -86,6 +86,8 @@ gangtise ai earnings-review-check --data-id <id>
|
|
|
86
86
|
- `--period`:`年份+报告期`,如 `2025q3`(q1/interim/q3/annual),仅 A 股,覆盖最近 6 期
|
|
87
87
|
- `--wait`(**推荐**):阻塞等待到出结果(最长约 5 分钟:14 次指数退避轮询 5s→30s,累计 ≈316s)——**用它时把工具/命令超时设到 ≥360s**,否则外层先超时
|
|
88
88
|
- 不带 `--wait` 的手动轮询:① `earnings-review` → 拿 `{dataId, status, hint}` → ② 间隔 ~30s `*-check`(预算 ~2-3 分钟)→ pending 继续 → 多次仍 pending 交用户稍后手动 check
|
|
89
|
+
- 这一步的 `{dataId, status, hint}` 同样受 `--format` / `--output` 控制,**加了 `--output` 就会落盘**,脚本可以直接从文件里取 `dataId` 再去轮询
|
|
90
|
+
- `*-check` 返回「还在生成中」时同样落盘(`{dataId, status: "pending", hint}`),所以轮询脚本每一轮都能从同一个文件读状态,不必区分「出结果了」和「还没好」两种取法
|
|
89
91
|
- 错误码:`140001`(旧 `410110`)生成中,继续等待;`140002`(旧 `410111`)生成失败,终态不重试。CLI 两代码都识别
|
|
90
92
|
|
|
91
93
|
## 观点 PK `ai viewpoint-debate`(异步)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
通用:所有命令都需 `--security-code`(如 `600519.SH`,注意是 `--security-code` 不是 `--security`)。`--field` 可重复,可用字段见 `references/fields.md`;A / 港 / 美股三大报表命令都在省略 `--field` 时返回完整报表,指定后只保留基础字段与所选科目。
|
|
4
4
|
|
|
5
|
-
**`--field` 字段名必须核对**:三大报表遇到不存在的字段名会补 `null`(安全),但 `main-business` / `valuation-analysis`
|
|
5
|
+
**`--field` 字段名必须核对**:三大报表遇到不存在的字段名会补 `null`(安全),但 `main-business` / `valuation-analysis` 是**只丢值、字段名照请求回显**,`fieldList` 比行长,按位置拍平会把值贴到错误的字段上。CLI 长度不匹配就直接失败(退出码 1),不输出错位数据。不确定字段名就别传 `--field`。
|
|
6
|
+
|
|
7
|
+
⚠️ **别把这个行为套到 `quote` 系上**——那边是**字段名和值一起消失**、列数与值数始终对得上,CLI 的处理也不同(标 `partial` + `missingFields`、退出码 3)。两族的完整对照见 `references/fields.md` 顶部。
|
|
6
8
|
|
|
7
9
|
---
|
|
8
10
|
|
|
@@ -94,7 +94,21 @@ gangtise indicator time-series --indicator <code> [--indicator <code2>] \
|
|
|
94
94
|
- `--indicator` / `--security`:同上,但**只允许「多指标 × 单证券」或「单指标 × 多证券」**,不能两边都多个(要多 × 多用 `cross-section`,否则报 `100003`「仅支持多指标单证券或单指标多证券」)。**单指标时 `--security` 才能传板块 ID**;多指标时只能传一个证券代码、不得传板块 ID
|
|
95
95
|
- `--start-date` / `--end-date`(**均必选**):区间端点 `yyyy-MM-dd`。时序的时间范围由这两个参数统管,**不要**再用 `--indicator-param` 传 `tradeDate` 这类单日期参数
|
|
96
96
|
- ⚠️ **同一 `indicatorCode` 挂多套参数:截面和时序都不支持,这是设计如此,要拆成两次调用**。只有 `screener` 支持(它把指标绑到不同变量 `F1`/`F2` 上,天然可区分)。CLI 也表达不了该请求——`--indicator-param` 按 code 建 Map,同 code 的多组参数会被合并成一组,与服务端设计一致。走 `raw call` 真发出去会报 `100003「指标 xxx 重复配置」`
|
|
97
|
-
- `--calendar-type`:日期类型 `ND`(自然日)/`TD`(
|
|
97
|
+
- `--calendar-type`:日期类型 `ND`(自然日)/`TD`(交易日)/`WD`(工作日)。`TD` 且跨市场时,`date` 列是各市场交易日的**并集**。服务端默认 `ND`
|
|
98
|
+
|
|
99
|
+
**不传时 CLI 会自动选**(v0.40.0 起):发请求前用 `indicator search`(免费)读每个指标的 `parameterList`——**去重后每个指标码各查一次**(并发,`--indicator` 只给一个时就是一次)——
|
|
100
|
+
|
|
101
|
+
| 本次请求的指标 | CLI 发什么 | 为什么 |
|
|
102
|
+
| :-- | :-- | :-- |
|
|
103
|
+
| **全部**都声明 `tradeDate`、且**都不**声明 `reportDate` / `fiscalYear` | `TD`,并在 stderr 说明一句 | 行情 / 日频估值序列,交易日轴上一格不少,还省掉周末空行 |
|
|
104
|
+
| 任一指标声明 `reportDate` 或 `fiscalYear` | 不发(服务端按 `ND`) | 见下面那条红字 |
|
|
105
|
+
| `parameterList` 为空、查不到该 code、或这次查询失败 | 不发(服务端按 `ND`) | 拿不准就退回服务端默认,不赌 |
|
|
106
|
+
|
|
107
|
+
**显式传了 `--calendar-type` 就完全按你给的发**,不查也不改。
|
|
108
|
+
|
|
109
|
+
- 🔴 **两个方向的代价不对称,所以自动选择只往 `ND` 一边兜底**:报告期末常落在非交易日(如 2024-03-31、2024-06-30 均为周日),`TD` 的日期轴里没有那一列,报告期类指标于是**整行全 `null`、退出码 0、不报错**,读起来像「这只票没有这项财务数据」。**选 `ND` 选错只是多花单元格,选 `TD` 选错是静默丢数。**
|
|
110
|
+
- **混合两类指标时一律 `ND`**:一次请求只有一个 `calendarType`,管不到单个指标——`ND` 下两类都取得到(行情列在周末是 `null`,本就该是),`TD` 下行情列正常而报告期列整行全空。自动选择按的就是这条:只要有一个报告期类指标,整条走 `ND`
|
|
111
|
+
- **`TD` 能省多少**:A 股一年约 240~245 个交易日,所以同一区间 `ND` 的日期列大约是 `TD` 的 **1.5 倍**,多出来的那三分之一全是非交易日的 `null`。换算到 30000 上限就是「单指标拉一年,`TD` 能带的证券数比 `ND` 多约一半」。**要精确值就用自己的区间各跑一次比 `dates` 长度**,别套这里的比例——交易日数逐年不同
|
|
98
112
|
- `--currency` / `--scale`:同 `cross-section`(含根级 `--scale` 的污染坑)
|
|
99
113
|
- **输出(宽表)**:每行一个日期,列为 `date / <各序列名>…`;序列在「单指标」时是各**证券**,在「单证券多指标」时是各**指标**。**板块 ID 算多证券**——传 1 个 `sectorId` 服务端会展开成 N 只成分股,列就是这 N 只(如中信白酒 → 19 列)
|
|
100
114
|
- **`--key-by name|code`**(默认 `name`):同 `cross-section`;`code` 模式下单证券列=各 `indicatorCode`、多证券列=各 `securityCode`,批量按 code 回填用它
|
|
@@ -192,6 +206,19 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
|
192
206
|
|
|
193
207
|
## 必填参数与错误码(取数前必读)
|
|
194
208
|
|
|
209
|
+
### 🔴 单次调用的单元格上限:30000
|
|
210
|
+
|
|
211
|
+
`cross-section` 与 `time-series` 各有一个**单次返回单元格总数不得超过 30000** 的硬上限,超出直接报 `100006 QUERY_LIMIT_EXCEEDED`(msg 写明限额),不返回部分结果、不截断。口径就是返回矩阵的元素个数:
|
|
212
|
+
|
|
213
|
+
| 接口 | 单元格数 | 例 |
|
|
214
|
+
| :-- | :-- | :-- |
|
|
215
|
+
| `cross-section` | **证券数 × 指标数** | 5000 只 × 6 指标 = 30000,可以;5001 只 × 6 指标 = 30006,被拒 |
|
|
216
|
+
| `time-series` | **序列数 × 日期数**(序列 = 单指标时的证券数,或单证券时的指标数) | 300 只 × 100 天 = 30000,可以;301 只 × 100 天 = 30100,被拒 |
|
|
217
|
+
|
|
218
|
+
**撞上限就按这个乘积拆批**:截面拆证券或拆指标,时序拆证券或缩日期区间,分批取回后自己合并。
|
|
219
|
+
|
|
220
|
+
⚠️ **两处最容易撞**:① `--security` 传板块 ID 时服务端会展开成全部成分股,实际证券数远大于写进命令的条数(全 A 股板块是五千多只,配 6 个指标就到顶);② 时序的日期轴由 `--calendar-type` 决定,`ND` 的日期列约是 `TD` 的 1.5 倍(周末与节假日各占一列),同一个查询在 `ND` 下更容易超限——纯行情序列走 `TD` 就省下这部分(不传时 CLI 会自动判,见上文)。
|
|
221
|
+
|
|
195
222
|
**缺数据 vs 代码写错**:
|
|
196
223
|
|
|
197
224
|
**缺数据一律保留行列并给一个占位单元格**,连单指标 × 单证券的 1×1 缺口也有行有列、不是空表。判据是**代码认不认识**,跟有没有数据无关:
|
|
@@ -68,7 +68,11 @@ gangtise vault wechat-chatroom-list [--room-name <name>] [--from <n>] [--size <n
|
|
|
68
68
|
- 省略 `--size` 拉全量(接口返回 `total`,CLI 按 total 并发翻页);传 `--size N` 只取前 N 条。单页最大 50
|
|
69
69
|
- 返回字段:`total`(总条数)/ `chatroomName` / `chatroomId`
|
|
70
70
|
|
|
71
|
-
## 自选股股票池 `vault stock-pool
|
|
71
|
+
## 自选股股票池 `vault stock-pool-*`
|
|
72
|
+
|
|
73
|
+
查询两个、增删改五个,全部免费,只操作当前账号本人的数据。
|
|
74
|
+
|
|
75
|
+
### 查询
|
|
72
76
|
|
|
73
77
|
```bash
|
|
74
78
|
gangtise vault stock-pool-list
|
|
@@ -80,3 +84,34 @@ gangtise vault stock-pool-stocks [--pool-id <id>]
|
|
|
80
84
|
- `--pool-id`:股票池 ID,可重复;不传默认 `all`(返回所有池中的非重复证券)
|
|
81
85
|
- 传入 `--pool-id all` 等同于全量查询,最多返回 10000 只
|
|
82
86
|
- 返回字段:`securityCode` / `securityName`
|
|
87
|
+
|
|
88
|
+
### 增删改
|
|
89
|
+
|
|
90
|
+
🔴 **这五个命令会改动账号数据**,是本 CLI 仅有的写操作;`--pool-id` 一律取自 `stock-pool-list`。
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gangtise vault stock-pool-create --name <名称>
|
|
94
|
+
gangtise vault stock-pool-rename --pool-id <id> --name <新名称>
|
|
95
|
+
gangtise vault stock-pool-add-stock --pool-id <id> --security <code> [--security <code>...]
|
|
96
|
+
gangtise vault stock-pool-remove-stock --pool-id <id> --security <code> [--security <code>...]
|
|
97
|
+
gangtise vault stock-pool-delete --pool-id <id> [--pool-id <id>...] --yes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `stock-pool-create`:建池,返回新池的 `poolId` / `poolName`
|
|
101
|
+
- **池名上限 10 个字符**(按字符计,中文算 1 个),超出返回 `230007`。⚠️ 在 Gangtise 终端里建的老池名可以更长,通过本接口建池 / 改名则一律受这个上限约束
|
|
102
|
+
- **池名不能与已有池重复**,重名返回 `230006`;判重是整串精确比较——首尾空格不会被去掉(`" A "` 与 `"A"` 可并存),大小写也不归一(`abc` 与 `ABC` 可并存)
|
|
103
|
+
- 每个账号最多 30 个池,达到上限后返回 `230003`
|
|
104
|
+
- 这条命令超时或 5xx **不会自动重发**(重发一个其实已经建成的请求只会撞重名报错);超时后先 `stock-pool-list` 看池建成没有,再决定重来
|
|
105
|
+
- `stock-pool-rename`:只改名,池内自选股不受影响;新名的长度与重名规则同上(改成该池自己当前的名字算成功)
|
|
106
|
+
- `stock-pool-add-stock` / `stock-pool-remove-stock`:批量加 / 批量移出,`--security` 可重复或逗号分隔;单池上限 10000 只
|
|
107
|
+
- 代码要带市场后缀且**大小写敏感**:`600519.SH` / `00700.HK` / `AAPL.O` 可混在一次请求里;`600519`(缺后缀)、`600519.sh`、`aapl.o`、`700.HK`(港股要 5 位)都会进 `failList` 报「证券代码不存在」,而不是报错退出
|
|
108
|
+
- 首尾空格会被自动去掉,列表内重复的同一代码会去重(成功项只计一次)
|
|
109
|
+
- `stock-pool-delete`:批量删池,**必须显式加 `--yes`**(不加会直接报错退出,不发请求)
|
|
110
|
+
- 删池会同时移除池内全部证券的关注关系,**不可恢复**;个股的投资笔记独立保留,不受影响
|
|
111
|
+
- **`raw call vault.stock-pool.delete` 同样要 `--yes`**——换成 raw 入口不会跳过这道确认
|
|
112
|
+
|
|
113
|
+
**部分失败会被标出来**:`add-stock` / `remove-stock` / `delete` 是逐条处理的——证券代码不存在这类单条失败,服务端仍返回成功信封,把失败明细放进 `failList`(`successList` 里是成功的那些)。CLI 检测到 `failList` 非空时在 stderr 列出失败项与原因、给结果标 `partial`、**退出码 3**;全部成功才是退出码 0。批量脚本按退出码判断即可,不必自己解析 `failList`。**`raw call` 打这三个端点时判定完全相同**,同一份响应不会一个入口退 3、另一个退 0。
|
|
114
|
+
|
|
115
|
+
**幂等**:重复加已在池内的证券、移出本就不在池内的证券、删不存在的 `poolId`,都算成功并计入 `successList`,不会报错。唯一不幂等的是 `stock-pool-create`(见上面的重名规则)。
|
|
116
|
+
|
|
117
|
+
**返回字段**:`create` / `rename` 返回 `poolId` / `poolName`;`add-stock` / `remove-stock` / `delete` 返回 `successList[]` + `failList[]{securityCode 或 poolId, failReason}`。
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
| `110001` / `110002` | 日期格式错(msg 带字段名)/ 起晚于止。⚠️ **服务端对多种格式做宽松解析**(`2026/07/01`、`20260701`、`07/01/2026` 都能被接受),所以「没报 110001」不等于「格式被按你的意思理解了」 | — | 按参数名:`--*-date` 用 `YYYY-MM-DD`、`--*-time` 用 `YYYY-MM-DD HH:mm:ss`;`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒 |
|
|
23
23
|
| `120001` | 证券代码无效——msg 带原因(「非有效A股」)。Fundamental 系与 `quote day-kline`/`realtime`/`minute-kline`/`fund-flow` 都会报;**旧版 `day-kline-hk`/`day-kline-us`/`index-day-kline` 静默返回空** | — | `reference securities-search` 确认代码与后缀(`600519.SH` / `00700.HK`) |
|
|
24
24
|
| `110003` | **超出账号数据权限的时间范围**。范围按账号等级而定、**不是平台常量**;窗口按**账号**配、不按接口配 | — | 把日期移进权限范围内;整个区间都早于下界时缩短窗口无用(`--fiscal-year 2015` 无论怎么缩都报错);**换接口绕不过去**,要更长历史联系客户经理开通 |
|
|
25
|
-
| `100006` | 查询/下载数量超限(旧码 `430007`);`fund-flow`
|
|
25
|
+
| `100006` | 查询/下载数量超限(旧码 `430007`);`fund-flow` 全市场不传日期即此码;**`indicator cross-section` / `time-series` 单次单元格数超 30000** 也是此码(msg 写明限额) | — | 缩短日期范围或调小 `--size`/`--limit`;全市场场景应已自动分片;EDE 按「截面=证券数×指标数、时序=证券数或指标数×日期数」拆批 |
|
|
26
26
|
| `240001` | 财报期未披露或超出查询期(`earnings-review` 提交阶段就报,**不扣积分**) | — | 换更早的 `--period`(`2025q3` → `2025interim`) |
|
|
27
27
|
| `250001` | 不支持的数据源(旧码 `433007`) | — | 检查 `resourceType + sourceId` 组合 |
|
|
28
28
|
| `999011` | 开发账号凭证无效(旧码 `8000014`/`8000015`,已合并,不区分 AK 错还是 SK 错) | 登录即失败,**不重试** | 检查 `GANGTISE_ACCESS_KEY`/`GANGTISE_SECRET_KEY` 是否写反或未 export |
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
| `0000001007` | 请求未携带 Bearer token | — | 检查 `GANGTISE_TOKEN` / AK/SK 是否已 export |
|
|
33
33
|
| `900002` | **请求方法不正确**(msg「请求类型有误」,HTTP 405) | — | `raw call` 时确认该 endpoint 是 GET 还是 POST |
|
|
34
34
|
| `140002` | 终态参数错:AI 异步生成失败,或 `indicator` 的指标必填参数缺失 / 枚举越界 / 表达式语法错 | **不重试**(终态码) | 按 msg 改参数重提;EDE 的参数名与枚举读 `indicator search --format json` 的 `parameterList` |
|
|
35
|
+
| `230003` | 股票池数量已达账号上限(30 个) | — | 先 `vault stock-pool-delete` 删掉不用的池再建 |
|
|
36
|
+
| `230006` | 建 / 改股票池撞池名重复 | — | 先 `vault stock-pool-list` 看现有名称再换一个 |
|
|
37
|
+
| `230007` | 股票池名超过 10 个字符(中文算 1 个) | — | 缩短名称;终端里建的老池可以更长,本接口不行 |
|
|
35
38
|
|
|
36
39
|
## ⚠️ 几类「不报错」的坑(最难发现,逐条都关系到拿没拿到对的数)
|
|
37
40
|
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
- **财报接口的日期按「报告期末」过滤**,不是公告日:`fundamental balance-sheet` 等的 `--start-date`/`--end-date` 匹配的是 `endDate` 字段(如 `20200630`);公告日看 `earliestAnncDate`(首次公告日,做时点对齐用这个)而不是 `announcementDate`。**查某期财报要传季度末日期**(`2020-06-30` / `2020-03-31` / `2020-09-30` / `2020-12-31`);传 `2020-07-01` 这类非报告期日期会返回 0 行,属正常行为,不是没数据
|
|
40
43
|
- **非法证券代码**:`quote day-kline` / `realtime` / `minute-kline` / `fund-flow` 与 Fundamental 系都会报 `120001`,按报错核对后缀即可。⚠️ **三个已下线的旧端点 `quote day-kline-hk` / `day-kline-us` / `index-day-kline` 则返回空结果**,与"该票该区间真无数据"无法区分——**用这三个拿到空结果时先回头核对代码后缀**。它们的能力已并入统一 `day-kline`(支持 A 股 / 港股 / 美股 / 沪深 ETF / 交易所指数 / 概念指数 `.GT` / 行业指数 `.CI` `.SWI` / 20 个全球指数),新代码直接用 `day-kline`
|
|
41
44
|
- **枚举值拼错基本都会报 `100005`,但「字段名不被该端点支持」仍然静默**:纪要 `summary`、三个公告 list、路演 `roadshow`、调研 `site-visit`、`insight research`、`foreign-report`、`official-account` 的 `--search-type` / `--rank-type` / `--category` / `--market` 等传非法值报 `100005`。⚠️ **剩下的口子是「该端点根本没有这个参数」**:传一个端点不支持的字段名(如给 `opinion` 传 `searchType`——它只有 `--rank-type`)会被静默丢弃、按未传处理,返回未经筛选的全库而不报错。**CLI 本地拦截**:全部 `--search-type` / `--rank-type`、全部 `--file-type`(`foreign-report` 为 1–4、其余 1–2)、`pamirs-summary` 的 `--category` / `--market`、`--top` 上限、以及 `reference securities-search` / `institution-search` / `official-account-search` 的 `--category`。**仍未覆盖**:`insight research/summary --category`、`--market`、`--source`、`--llm-tag` 等仍是自由字符串,拼错不报错也不生效——这些要自己核对。**拼错的筛选条件会伪装成"结果正常",枚举拼写要自己保证**
|
|
45
|
+
- **股票池写操作的单条失败不报错**:`vault stock-pool-add-stock` / `stock-pool-remove-stock` / `stock-pool-delete` 是逐条处理的,证券代码不存在这类单条失败仍返回成功信封(`code` 为 `000000`),失败明细在 `failList` 里。**CLI 已拦**:`failList` 非空时 stderr 列出失败项、结果标 `partial`、退出码 3。绕过 CLI 直连接口时要自己读 `failList`,别只看 `code`
|
|
42
46
|
- **`viewpoint-debate` 传敏感内容不会被提前拦截**——不返回 `240002`,而是照常受理、扣满 50 积分、生成阶段才以 `410111` 失败。**提交前自己把关措辞**
|
|
43
47
|
- **`ai one-pager` 的非法 `mode` 被静默忽略**,照常生成并扣 50 积分
|
|
44
48
|
|
|
@@ -356,3 +356,32 @@
|
|
|
356
356
|
4. unzip 后读 file.md(图片在 images/);正文长先 wc -l / head 采样再呈现
|
|
357
357
|
5. 超时不要重跑 file-parse(会重复扣费),用 file-parse-check 拿同一个 taskId 的结果
|
|
358
358
|
```
|
|
359
|
+
|
|
360
|
+
## 例 20:维护自选股股票池(本 CLI 仅有的写操作)
|
|
361
|
+
|
|
362
|
+
**用户**:"把今天筛出来的这几只建个池叫「AI算力观察」"
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
1. 路由 → vault stock-pool-create → stock-pool-add-stock
|
|
366
|
+
2. Pre-flight:🔴 这两个命令会改动用户账号数据 → 先复述「建一个叫 X 的池,把这 N 只加进去」再执行
|
|
367
|
+
公司名先 reference securities-search 换成代码,不要猜
|
|
368
|
+
3. gangtise vault stock-pool-create --name "AI算力观察" --format json
|
|
369
|
+
→ { poolId, poolName };重名会被拒(230006),名称超过 10 个字符被拒(230007),
|
|
370
|
+
撞到就先 stock-pool-list 看现有名称,跟用户确认换名还是往老池里加
|
|
371
|
+
4. gangtise vault stock-pool-add-stock --pool-id <poolId> \
|
|
372
|
+
--security 600519.SH --security 000858.SZ --format json
|
|
373
|
+
→ { successList[], failList[] }
|
|
374
|
+
5. 🔴 看退出码:0 = 全部加进去了;3 = 有失败项,stderr 已列出是哪几只(多为代码写错),
|
|
375
|
+
把失败的几只复述给用户,别只报「已完成」
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**用户**:"这个池不要了,删掉"
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
1. 路由 → vault stock-pool-list 拿 poolId → vault stock-pool-delete
|
|
382
|
+
2. Pre-flight:🔴 删池会连带清掉池内全部关注关系且不可恢复(投资笔记不受影响)
|
|
383
|
+
→ 复述池名与池内证券数,等用户确认后才加 --yes
|
|
384
|
+
3. gangtise vault stock-pool-delete --pool-id <poolId> --yes
|
|
385
|
+
不加 --yes 时 CLI 直接报错、不发请求——**看到这个报错不要自动补 --yes 重跑**,
|
|
386
|
+
那是留给用户点头的一步
|
|
387
|
+
```
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
> 按命令分组,`--field` 参数可重复传入。不传 `--field` 时返回全部字段。`quote` 系传了 `--field` 就只回点名的列、不自动附带身份列(`fund-flow` 除外,它自动带 `securityCode` / `tradeDate`):日 K 要一起写进 `securityCode` / `tradeDate`,分钟 K 是 `securityCode` / `tradeTime`,实时行情是 `securityCode`。
|
|
4
4
|
|
|
5
|
+
## 🔴 字段名写错时,两族接口的表现完全不同
|
|
6
|
+
|
|
7
|
+
`--field` 里写了一个不存在的字段名(拼错、或该字段已下线),**没有任何接口会报错**,但丢的东西不一样——这决定了你能不能按位置拍平结果:
|
|
8
|
+
|
|
9
|
+
| 接口 | 响应里发生什么 | CLI 怎么处理 |
|
|
10
|
+
| :-- | :-- | :-- |
|
|
11
|
+
| `quote realtime` / `day-kline` / `minute-kline` / `fund-flow` | **字段名和值一起消失**,`fieldList` 里也没有它,列数与值数始终对得上 | 比对请求与返回的列名,缺列标 `partial` + `missingFields`、在 stderr 点名、**退出码 3** |
|
|
12
|
+
| `fundamental main-business` / `valuation-analysis` | **字段名照请求回显在 `fieldList` 里,行里却少一个值**,两者长度对不上 | 检测到长度不匹配**直接报错退出 1**,不输出任何可能错位的数据 |
|
|
13
|
+
|
|
14
|
+
**第二族才是危险的那个**:`fieldList` 比行长,按下标对位会让缺口之后的每一个值都贴到错误的字段名上——一列合理的数字配着错误的表头,肉眼看不出来。CLI 因此选择整条失败而不是给你一份错位的表。
|
|
15
|
+
|
|
16
|
+
**结论一样**:`--field` 的字段名以本文件为准,不确定就别传(不传即返回全部字段,永远安全)。
|
|
17
|
+
|
|
18
|
+
绕过 CLI 直连接口时**两族要查的东西不同,别只做一种**:
|
|
19
|
+
|
|
20
|
+
- `quote` 系:`fieldList` 与行**始终等长**,只比长度什么都查不出来——要拿**请求的字段名**去比**返回的 `fieldList`**,少了哪个就是那个名字没被认出来
|
|
21
|
+
- `fundamental` 两个:`fieldList` 会把你写的名字原样回显,所以对比请求没用——要比 **`fieldList` 长度与每行的值个数**,对不上就不能按下标取值
|
|
22
|
+
|
|
5
23
|
---
|
|
6
24
|
|
|
7
25
|
## Quote 行情
|
|
@@ -18,7 +36,9 @@
|
|
|
18
36
|
> 2. 🔴 **美股的 `quote realtime`**。它的 `amount` 是 **`null`**(接口不提供该字段,见下方 realtime 字段表),自检算不出来。**要美股的实时成交额,只能用别的口径**(收盘后用 `day-kline`,或 EDE `qte_amt`)。
|
|
19
37
|
> 3. **全球指数**(`SPX.SPI` 等 20 个):realtime 与分钟 K 的 `volume` / `amount` 都是 `null`,日 K 只有 `amount` 是 `null`。
|
|
20
38
|
>
|
|
21
|
-
>
|
|
39
|
+
> 🔴 **分钟线加总不等于日线,别把它当校验条件**。正常交易日 241 根,多数证券多数日子里 Σ分钟 `volume` 与日线逐位相等、`amount` 只差末位舍入;但**个别历史交易日的分钟数据口径与日线不一致**,那几天怎么加都对不上,两条序列各自稳定、都不报错,历史数据也不会回改。
|
|
40
|
+
>
|
|
41
|
+
> **口径**:成交量 / 成交额的统计一律**以日线为准**,分钟线用于日内分布。撞到对不上时,那是那一天的数据,不是你的加总代码有问题——**更不要用分钟加总去反推或"修正"日线**。
|
|
22
42
|
>
|
|
23
43
|
> (`14:58` / `14:59` 恒为 0 是**正常的**:14:57 后进入收盘集合竞价,只申报不撮合,成交在 `15:00` 那一根一次性发生——**收盘那根在序列里**,241 根的末根就是它。)
|
|
24
44
|
|
|
@@ -169,6 +189,8 @@
|
|
|
169
189
|
|
|
170
190
|
### 主营业务 (`fundamental main-business`)
|
|
171
191
|
|
|
192
|
+
> ⚠️ `--field` 写错字段名时**只丢值、字段名照回显**,CLI 会因长度不匹配报错退出 1——见本文顶部「字段名写错时,两族接口的表现完全不同」。
|
|
193
|
+
|
|
172
194
|
| 字段 | 含义 | 字段 | 含义 |
|
|
173
195
|
|------|------|------|------|
|
|
174
196
|
| `opRevenue` | 营业收入 | `opRevenueYoy` | 营业收入同比增速 |
|
|
@@ -180,6 +202,8 @@
|
|
|
180
202
|
|
|
181
203
|
### 估值分析 (`fundamental valuation-analysis`)
|
|
182
204
|
|
|
205
|
+
> ⚠️ `--field` 写错字段名时**只丢值、字段名照回显**,CLI 会因长度不匹配报错退出 1——见本文顶部「字段名写错时,两族接口的表现完全不同」。
|
|
206
|
+
|
|
183
207
|
| 字段 | 含义 | 字段 | 含义 |
|
|
184
208
|
|------|------|------|------|
|
|
185
209
|
| `value` | 原始值 | `percentileRank` | 分位点 |
|
|
@@ -82,6 +82,11 @@ CLI 自动处理 envelope:`{code, msg, data}` 信封会按 `code === "000000"`
|
|
|
82
82
|
| vault my-conference-download | 文件路径(stdout) | — |
|
|
83
83
|
| vault wechat-message-list | `{list, total}` | `list[].msgId` / `list[].content`(正文)/ `list[].url` / `list[].msgTime` / `list[].wechatGroupName` / `list[].speakerName` / `list[].category` / `list[].tagList[].tagCode` / `list[].securityList[].securityCode` / `list[].quoteMsg.quoteContent`(引用消息,无引用为 `null`)。**字段名是 `content` / `url`**,不是 `msgContent` / `contentUrl` |
|
|
84
84
|
| vault wechat-chatroom-list | `{list, total}` | `list[].chatroomName` / `list[].chatroomId` |
|
|
85
|
+
| vault stock-pool-list | 数组 | `[].poolId`(后续命令的 `--pool-id`)/ `[].poolName` |
|
|
86
|
+
| vault stock-pool-stocks | 数组 | `[].securityCode` / `[].securityName`。空池返回 `[]` |
|
|
87
|
+
| vault stock-pool-create / stock-pool-rename | `{poolId, poolName}` | 新建 / 改名后的池 ID 与名称 |
|
|
88
|
+
| vault stock-pool-add-stock / stock-pool-remove-stock | `{successList, failList}` | `successList[]` 是规范化后的证券代码(含已在池内 / 本就不在池内的幂等成功项);`failList[]{securityCode, failReason}`。**外层 `code` 恒为 `000000`,逐条失败只在 `failList` 里**——CLI 检测到非空时标 `partial`、退出码 3 |
|
|
89
|
+
| vault stock-pool-delete | `{successList, failList}` | 同上,但键是 `failList[].poolId`;删不存在的池算幂等成功,计入 `successList` |
|
|
85
90
|
| alternative edb-search | `{list: [...]}` 指标列表 | `indicatorId` / `indicatorName` / `dataSource` / `frequency` / `unit` |
|
|
86
91
|
| alternative edb-data | 列表,每行 `{date, <indicatorId>: value, ...}` 宽表 | `date` + 每个 `--indicator-id` 一列(该日指标值) |
|
|
87
92
|
| alternative concept-info | `{conceptId, conceptName, ...}`(单对象,**非列表**) | `conceptName` / `definition` / `investmentLogic` / `industrySpace` / `competitiveLandscape` / `keyEvents[].date` / `keyEvents[].content`;文本字段未配置为 `null` |
|