gangtise-openapi-cli 0.48.0 → 0.50.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 +40 -13
- package/dist/src/cli.js +9 -3
- package/dist/src/commands/ai.js +2 -2
- package/dist/src/commands/fund.js +5 -3
- package/dist/src/commands/groups.js +2 -1
- package/dist/src/commands/indicator.js +9 -14
- package/dist/src/commands/insight.js +10 -3
- package/dist/src/commands/quote.js +19 -0
- package/dist/src/commands/shared.js +2 -1
- package/dist/src/commands/update.js +30 -0
- package/dist/src/commands/vault.js +49 -4
- package/dist/src/core/endpoints.js +40 -4
- package/dist/src/core/errors.js +26 -12
- package/dist/src/core/etfComponents.js +358 -0
- package/dist/src/core/normalize.js +1 -1
- package/dist/src/core/selfUpdate.js +94 -0
- package/dist/src/core/updateCheck.js +50 -14
- package/dist/src/version.js +5 -3
- package/gangtise-openapi/SKILL.md +25 -19
- package/gangtise-openapi/references/commands/ai.md +1 -1
- package/gangtise-openapi/references/commands/fund.md +6 -2
- package/gangtise-openapi/references/commands/indicator.md +41 -20
- package/gangtise-openapi/references/commands/insight.md +4 -1
- package/gangtise-openapi/references/commands/quote.md +20 -0
- package/gangtise-openapi/references/commands/reference-and-lookup.md +1 -1
- package/gangtise-openapi/references/commands/vault.md +54 -8
- package/gangtise-openapi/references/errors.md +3 -3
- package/gangtise-openapi/references/examples.md +1 -1
- package/gangtise-openapi/references/fields.md +1 -1
- package/gangtise-openapi/references/response-schema.md +10 -5
- package/package.json +1 -1
- package/dist/src/core/calendarType.js +0 -44
package/README.md
CHANGED
|
@@ -6,18 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
README 仅列最近 5 个版本摘要:
|
|
8
8
|
|
|
9
|
+
- **v0.50.0 — 2026-11-01**:① **新增个股笔记**:`vault stock-note-list` 查询(全部 / 按证券 / 按笔记 ID)、`stock-note-add` 新增(一条关联 1~50 只证券)、`stock-note-update` 改写正文、`stock-note-delete` 删除(须 `--yes`),均免费。② **新增 `quote sector-fund-flow` 板块日资金流向**(免费;按当日成分股加总、只计沪深京;撞 `100006` 要缩区间或减板块 / 字段,调小 `--limit` 无效)。③ `vault stock-pool-list` / `stock-pool-stocks` 加 `--space-type 2` 查所属租户的股票池。④ 路演 / 调研 / 策略会 / 论坛加 `--publish-start-time` / `--publish-end-time`,按收录时间筛选,可与举办时间同时用。⑤ `indicator time-series` 不传 `--calendar-type` 时由服务端按指标定日期轴(不再先查 `indicator search`);区间里没有报告期末时报告期类指标返回空结果(此前是逐日 `null` 行);⚠️ 一致预期 `frcst_*` 要逐日序列须显式传 `ND`。⑥ ETF 成分补代码在名称查询失败、A / H 定不了等情形下留 `null`,不再按其余行推断;A / H 价格都对得上时按其余行所在的单一市场取。⑦ 指标参数报错的改法提示按服务端新写法修正。⑧ 文档:量比、自由流通、流通股本与市值、AH 溢价率、涨跌停、TTM 利润类等 EDE 指标;⚠️ 基金收益率同类排名的分母不一定等于所属分类的基金数,同类比较前先核对。
|
|
10
|
+
|
|
11
|
+
- **v0.49.0 — 2026-10-28**:① **新增 `gangtise update`**:一条命令更新到最新版(从 npmjs.org `npm install -g`),并替换 `~/.claude` / `~/.codex` / `~/.openclaw/workspace` / `~/.hermes` 下版本较旧的 Agent Skill 副本;`--check` 只查不装,`--no-skill` 不动 skill 副本。② **新版本提示**:在终端里运行任何命令时,若已有新版本,结束后 stderr 提示一行(同一新版本约一天一次);版本信息在后台刷新、命令不等网络,脚本 / Agent / CI 中不提示,`GANGTISE_NO_UPDATE_CHECK=1` 关闭。③ **管理层情绪的 `publishTime` 现为北京时间**,与 `--start-time` / `--end-time` 同一口径;按 v0.48.0 说明「先加 8 小时」处理过的脚本要去掉这一步。④ **路演 / 调研 / 策略会 / 论坛列表的翻页去重改按各自的 ID**(`roadshowId` / `siteVisitId` / `strategyMeetingId` / `forumId`):跨页重复的行会被去掉、结果标为不完整(退出码 3),提示缩短时间范围重拉;`response-schema.md` 同步更正这四个列表的字段名。⑤ **公募基金**:`fund holder-structure` 的 `holderCount` 现为整数,按 v0.44.0 说明「先去千分位逗号再转数字」的处理要去掉(对整数做字符串替换会报错);`fund etf-pcf-components` 的接口只返回成分名称,CLI 按名称匹配补上 `componentCode`(带市场后缀,如 `600519.SH` / `00941.HK`;用该行收盘价核对,A+H 同名两行分别匹配;现金行等匹配不到的为 `null`,stderr 列出),`--no-component-code` 关闭。⑥ `ai security-clue` 的 `--query-mode` / `--gts-code` 补充帮助说明。
|
|
12
|
+
|
|
9
13
|
- **v0.48.0 — 2026-10-24**:① **新增管理层情绪三个命令**:`ai management-sentiment-indicator`(每场业绩会的正面回应程度 / 回复乐观度 / 回复确定度评分与按问题类型拆分)、`ai management-sentiment-conclusion`(情绪结论与较上一场的问答对照),各 5 积分/条、自动翻页;`ai management-sentiment-detail` 按列表返回的 `source.sourceId` 取逐条问答评分与依据,10 积分/次。⚠️ 返回的 `publishTime` 比北京时间早 8 小时(时间筛选按北京时间),按时刻展示或按日期归档前先加 8 小时。② **新增 `bond default-list` 违约债券**(0.4 积分/次):按违约日期区间(不超过 1 年)/ `--security` / `--issuer` 三选一;`--issuer` 一次一个、用发行人全称,简称只返回最匹配的一家主体,核对 `issuerName`。③ 文档:EDE 基金指标(分类、复权净值、区间收益与风险指标、同类排名),区间指标的参数须显式传。④ 依赖 `undici` 升级到 7.30.1(含上游安全修复)。
|
|
10
14
|
|
|
11
15
|
- **v0.47.0 — 2026-10-17**:① **`insight opinion list` 新增 `--category`(观点类别:业绩点评 / 异动点评 / 调研访谈更新 / 交易台 / 行业分析)与 `--broker-tag`(头部券商 / 新财富团队)**,返回行带 `categoryList` / `brokerTagList`。② **`--llm-tag` 改为 28 项投研业务标签**(强烈推荐、超预期、涨价、龙头等,取值见 `--help`);⚠️ **存量脚本要改**:业绩点评、头部券商、新财富团队改用 `--category earningsReview` / `--broker-tag topBroker` / `--broker-tag newFortune`,传给 `--llm-tag` 报 `100005`。同一选项传多个值取并集。③ `--with-content` 不支持 `--category` / `--broker-tag`(本地报错、不发请求),其 `--llm-tag` 仍只收旧的四个值;要按新标签筛又要正文,用 `list` + `detail`。④ **`bond daily-quote` / `bond valuation` 首行比起始日晚超过 14 天时 stderr 提示**(上市较晚或数据起点较晚;多只同查只看整批最早的首行,逐只起点按 `securityCode` 核对)。⑤ 文档:股东结构类 EDE 指标(`shr_*`),大股东系列须传 `rank`、户均持股系列须传 `shareType`。
|
|
12
16
|
|
|
13
17
|
- **v0.46.0 — 2026-10-14**:① **`reference securities-search --category bond`**:按代码或简称搜债券,代码用于 `bond` 命令组。⚠️ **可转债可能出现在任何 `--category` 的结果里**(按简称查股票时常排第一),**只取返回行 `category` 等于所查类别的那一行**;债券代码传给 `fund` 命令会返回空结果、不报错。② **场外基金可按名称搜到**(`--category fund` 同时返回 `.OF` 与场内代码)。③ **计费单价按平台最新价目更新**:一页通等 AI 生成 10/次、业绩点评 / 观点 PK 20/次、热点话题 10/篇、个股看点 1/条、路演四件套 10/条、独立观点列表 1/条、纪要 / 外资研报 / 我的会议下载 30/篇、公告下载 A 股 1 / 港美股 2、`edb-data` 30/指标;额度保护随之调整。④ **数据范围**:`SKILL.md` 按官方权限表逐命令列出正式 / 试用两档,规划查询区间按表来。⑤ **报错口径**:EDE 漏传必填的 `fiscalYear` 报 `100001`;`main-business` / `valuation-analysis` 的 `--field` 写错名字报 `100005`;`screener` 计费按筛选前范围保守估算。完整列表见 CHANGELOG。
|
|
14
18
|
|
|
15
|
-
- **v0.45.0 — 2026-10-07**:① **`vault drive-copy` 可复制整个文件夹**:`--folder-id` + `--target-parent-id`,连同子文件夹与文件复制到另一空间,返回 `{folderId, newFolderId}`;复制文件的写法不变,两组参数二选一。② **下载文件名**:响应同时给出 UTF-8 文件名与普通文件名时用 UTF-8 的那个。③ `GANGTISE_BASE_URL` 指向非本机地址却用 `http://` 时 stderr 提示一次(凭证会明文传输)。④ 文档:`bond daily-quote` / `bond valuation` 的数据可能比账号窗口短,**按年份取长序列时核对首行日期**;`bond announcement` 按日期查询时有代码的行都带后缀;基金只知道名称时怎么换代码;分钟 K 的可查历史是滚动的短窗口。
|
|
16
|
-
|
|
17
|
-
- **v0.44.0 — 2026-09-30**:**新增 `fund` 公募基金 18 个命令,一律 0.4 积分/次**(按次,与返回行数、基金只数无关):`basic-info` / `nav`(日频净值)/ `fee-rate` / `manager-info`(按姓名精确匹配,同名全返回)/ `manager-history` / `asset-size` / `holder-structure` / `top10-holders` / `asset-allocation` / `stock-portfolio` / `industry-allocation` / `bond-portfolio` / `bond-type-allocation` / `fund-portfolio` / `fund-type-allocation` / `etf-pcf-header` / `etf-pcf-components` / `etf-share-change`。`--security` 用带大写后缀的基金代码(场外 `.OF`,场内 `.SH` / `.SZ`),**代码不存在、不带后缀或后缀小写都返回空结果、不报错**。不分页:单次上限 10000 行,超出整批报 `100006`;日期两端都不传取账号可回溯窗口内的全部,起点早于窗口整批报 `110003`。**整族超时 / 5xx 不自动重发**(避免重复扣费)。取数前注意:各命令单位不同(元 / 万元 / 万份 / 万股);`holder-structure` 的 `holderCount` 是带千分位的字符串;持仓明细只返回证券简称、不返回代码;`fund-type-allocation` 季报期只含重仓基金。逐条见 `gangtise-openapi/references/commands/fund.md`。**另有几处行为变化**:只收一个值的参数重复传直接报错;代码类列表也按顿号 / 分号 / 空格分隔并去重;显式 `--size` 与 `fundamental earning-forecast` 的日期区间估算超过 1000 积分同样要 `--yes`;jsonl / csv 文件一律以换行结尾,csv 不再给 `-3.5%` 这类值加 `'`;全市场分片早于账号窗口的部分跳过而不中止;接口返回下载链接时不带 `--output` 也会下载;A 股公告与 `ai knowledge-batch` 只写日期的 `--end-time` 按当日 23:59:59 换算;`--indicator-param` 里的日期值与 `--date` 同样校验;单次请求另有总时长上限(`GANGTISE_TIMEOUT_MS` 的 2 倍,下载与上传 60 倍);环境变量设成空字符串按未设置处理。完整列表见 CHANGELOG。
|
|
18
|
-
|
|
19
19
|
### 历史里程碑
|
|
20
20
|
|
|
21
|
+
- **v0.44.0**:新增公募基金 `fund` 命令组 18 个命令(0.4 积分/次),额度保护覆盖显式 `--size`。
|
|
21
22
|
- **v0.43.0**:按条计费列表的额度保护(省略 `--size` 时估算超过 1000 积分须 `--yes`);翻页结果的跨页重复、版本变化与 `total` 封顶检测。
|
|
22
23
|
- **v0.42.0**:数值参数只收十进制写法(`0x10` / `1e3` 这类写法发请求前报错);接管道时保留退出码 `3` / `4`。
|
|
23
24
|
- **v0.41.0**:新增债券 12 个命令与云盘管理 9 个命令;观点列表改走 v2(列表只含摘要 1 积分/条,正文按 ID 另取);新增会议线索与联网搜索。
|
|
@@ -48,13 +49,17 @@ npm install -g gangtise-openapi-cli
|
|
|
48
49
|
gangtise --help
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
更新到最新版:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
|
-
|
|
55
|
+
gangtise update # 从 npmjs.org 安装最新版(npm install -g),并更新已安装的 Agent Skill 副本
|
|
56
|
+
gangtise update --check # 只查看有没有新版本,不安装
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
- `update` 命令从 v0.49.0 起提供,更早的版本先运行一次 `npm install -g gangtise-openapi-cli@latest`
|
|
60
|
+
- `gangtise update` 只更新通过 `npm install -g` 安装的 CLI;从仓库 clone 运行的,按原方式 `git pull` 后重新构建。npm 报权限错误(`EACCES`)时,按提示用 `sudo npm install -g …` 安装后,再运行一次 `gangtise update` 更新 Agent Skill 副本
|
|
61
|
+
- Agent Skill:`~/.claude/skills/`、`~/.codex/skills/`、`~/.openclaw/workspace/skills/`、`~/.hermes/skills/` 下版本低于新版的 `gangtise-openapi` 副本会被替换(**对副本做过的本地修改会被覆盖**);版本相同或更新的副本、符号链接、以及其他目录下的副本不动。加 `--no-skill` 则不碰 skill 副本
|
|
62
|
+
- 在终端里运行任何命令时,若已有新版本,命令结束后 stderr 提示一行(同一个新版本大约一天提示一次)。版本信息在后台刷新,命令本身不等待网络;输出不是终端时(脚本、Agent、CI)不提示。设 `GANGTISE_NO_UPDATE_CHECK=1` 可关闭提示与后台查询
|
|
58
63
|
|
|
59
64
|
本地开发:
|
|
60
65
|
|
|
@@ -80,6 +85,7 @@ export GANGTISE_PAGE_CONCURRENCY=5 # 翻页/分片并发数(默认 5,上
|
|
|
80
85
|
export GANGTISE_VERBOSE=1 # 打印每个请求的耗时与字节数
|
|
81
86
|
export GANGTISE_TIMEOUT_MS=30000 # 请求超时,整数毫秒(默认 30s;上限 1 小时;低于 1 秒或写法非法时用默认值;未按所写数值生效时在 stderr 提示)。按「多久没收到数据」计时,另有单次请求总时长上限:普通请求为它的 2 倍,下载与上传为 60 倍。one-pager 等生成类命令至少 120 秒、上传类至少 300 秒,设得更短不生效
|
|
82
87
|
export GANGTISE_TOKEN_CACHE_PATH=... # 覆盖 token 缓存路径(默认 ~/.config/gangtise/token.json)
|
|
88
|
+
export GANGTISE_NO_UPDATE_CHECK=1 # 关闭新版本提示与后台版本查询
|
|
83
89
|
```
|
|
84
90
|
|
|
85
91
|
环境变量设成空字符串(如 `GANGTISE_BASE_URL=`)按未设置处理,取默认值。`GANGTISE_BASE_URL` 指向非本机地址却用 `http://` 时,CLI 在 stderr 提示一次:登录与请求里的 AK / SK / token 会明文传输。
|
|
@@ -137,7 +143,7 @@ install_skill ~/.hermes/skills # Hermes
|
|
|
137
143
|
|
|
138
144
|
> 从仓库 clone 开发时,把 `$SKILL_SRC` 换成仓库内 `gangtise-openapi` 目录的绝对路径即可。
|
|
139
145
|
|
|
140
|
-
> **版本更新**:`SKILL.md` 头部的 `version` 字段与 CLI 版本一致。`
|
|
146
|
+
> **版本更新**:`SKILL.md` 头部的 `version` 字段与 CLI 版本一致。`gangtise update` 会一并替换上面四个目录里版本较旧的副本;装在其他目录的副本,更新 CLI 后重新执行上面这段,再看已安装副本的 `version` 是否与 `gangtise --version` 相同。对已安装副本做过的本地修改会被覆盖。
|
|
141
147
|
|
|
142
148
|
安装后,可以用自然语言触发,例如:
|
|
143
149
|
- "帮我查今天所有的研报"
|
|
@@ -184,6 +190,7 @@ install_skill ~/.hermes/skills # Hermes
|
|
|
184
190
|
| | `minute-kline` | 分钟K线——沪深A股 / ETF + 各类指数含全球指数(`--security` 可重复,逐只并发合并) |
|
|
185
191
|
| | `realtime` | 实时行情快照——A股/港股/美股个股 + 沪深 ETF + 各类指数含全球指数 |
|
|
186
192
|
| | `fund-flow` | A股个股日资金流向(沪深京;小/中/大/特大单 + 主力净流入) |
|
|
193
|
+
| | `sector-fund-flow` | 板块日资金流向(行业 / 概念 / 指数板块,按当日成分股加总,只计沪深京上市公司;免费) |
|
|
187
194
|
| **Fundamental** | `income-statement` / `balance-sheet` / `cash-flow` | A股三大财务报表(累计) |
|
|
188
195
|
| | `income-statement-quarterly` / `cash-flow-quarterly` | A股利润表/现金流量表(单季度) |
|
|
189
196
|
| | `income-statement-hk` / `balance-sheet-hk` / `cash-flow-hk` | 港股三大财务报表(中国会计准则) |
|
|
@@ -240,9 +247,11 @@ install_skill ~/.hermes/skills # Hermes
|
|
|
240
247
|
| | `record-list` / `record-download` | 录音速记列表与下载 |
|
|
241
248
|
| | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
|
|
242
249
|
| | `wechat-message-list` / `wechat-chatroom-list` | 群消息列表与群ID查询 |
|
|
243
|
-
| | `stock-pool-list` / `stock-pool-stocks` |
|
|
250
|
+
| | `stock-pool-list` / `stock-pool-stocks` | 自选股股票池列表与证券明细(`--space-type 2` 查所属租户的股票池) |
|
|
244
251
|
| | `stock-pool-create` / `stock-pool-rename` / `stock-pool-delete` | 建池 / 改名 / 删池(删池须加 `--yes`) |
|
|
245
252
|
| | `stock-pool-add-stock` / `stock-pool-remove-stock` | 批量关注 / 批量取消关注个股 |
|
|
253
|
+
| | `stock-note-list` | 个股投资笔记查询(全部 / 按证券 / 按笔记 ID) |
|
|
254
|
+
| | `stock-note-add` / `stock-note-update` / `stock-note-delete` | 新增笔记(一条关联 1~50 只证券)/ 改写正文 / 删除(删除须加 `--yes`) |
|
|
246
255
|
| **Indicator** | `search` | 证券级数据指标搜索(按名称匹配,返回 indicatorCode 及可传参数 parameterList) |
|
|
247
256
|
| | `cross-section` | 指标截面数据(多指标 × 多证券,单日快照;前置 `search` 拿 code) |
|
|
248
257
|
| | `time-series` | 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间) |
|
|
@@ -272,6 +281,7 @@ install_skill ~/.hermes/skills # Hermes
|
|
|
272
281
|
- `gangtise reference ...`
|
|
273
282
|
- `gangtise tool ...`
|
|
274
283
|
- `gangtise raw call ...` / `gangtise raw list`
|
|
284
|
+
- `gangtise update`(更新 CLI 与 Agent Skill 副本)
|
|
275
285
|
|
|
276
286
|
## 推荐工作流
|
|
277
287
|
|
|
@@ -309,7 +319,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
309
319
|
- **流式下载**:二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;不带 `--output` 时先写入临时文件,定好文件名后再改名。50MB PDF 内存占用近乎为零。
|
|
310
320
|
- **流式输出**:`--format jsonl` 或 `csv` 加 `--output <file>` 时,翻页 / 全市场分片 / 多证券分批请求的行**按到达顺序逐批写盘**,取数阶段不持有整份结果,内存不随行数增长;csv 先落临时行文件、收尾时按列并集写表头再转成 csv(磁盘两遍、内存不变)。jsonl / csv 无论写文件还是输出到终端都按批写出,每行以换行结尾。任一只 / 页 / 片失败,命令退出时后台已无取数与写盘,不留 `.part`。
|
|
311
321
|
- **导出元信息**:`csv` / `jsonl` 落盘时旁边生成 `<文件>.meta.json`——命令行、数据行数、列名、`complete`(写元信息时的判定:退出 3 即 `false`)、`total` / `partial` / `failedPages` / `failedShards` / `truncatedShards` / `droppedColumns` / `missingFields` / `duplicateRows` / `changedRows` / `totalCapped` 等全部完整性标记(以及不影响完整性判定的 `outOfWindowShards`)、抓取时间与时区、CLI 版本,以及数据文件的字节数 `bytes` 与内容哈希 `sha256`。命令行与结果里的 key / secret / token 类字段写成 `[redacted]`。元信息在数据文件发布之后才就位,导出失败不会留下新数据配旧元信息。**转交或归档前建议核一次**:`shasum -a 256 <文件>` 与元信息里的 `sha256` 相等,才说明这份元信息描述的就是旁边这份数据——两个进程同时导出到**同一个 `--output`** 时,最终文件一定是其中某一次的完整产物,但旁边的元信息有可能来自另一次,核哈希就能发现。**输的那一次自己也会报**:收尾时核对 `--output` 是否仍是本次写出的文件(仍是同一个文件就不必回读,否则回读比对哈希),不符就在 stderr 说明「这个文件不是本次命令的产物」并**退出码 4**。这两种格式的文件本身只有数据行,转交之后靠它核验是否完整;`json` 自带标记,不生成。
|
|
312
|
-
- **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**不重放的端点例外**(贵档:one-pager 等生成/提交类 + `tool file-parse` 提交 + 30/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`)+ `ai management-sentiment-detail`;`bond` 全部 13 个命令(多数按次,三个评级命令按条 / 按债券 / 按发行人)、`fund` 全部 18 个命令(按次)与按次计费的 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共
|
|
322
|
+
- **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**不重放的端点例外**(贵档:one-pager 等生成/提交类 + `tool file-parse` 提交 + 30/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`)+ `ai management-sentiment-detail`;`bond` 全部 13 个命令(多数按次,三个评级命令按条 / 按债券 / 按发行人)、`fund` 全部 18 个命令(按次)与按次计费的 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create`、`vault stock-note-add` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共 67 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;不计分的那几个是**重放会造成副作用或把成功报成失败**——股票池名不允许重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`;笔记没有防重机制,重发一个其实已经写入的新增会多出一条笔记;云盘允许同名,重发上传 / 新建 / 复制会多出一份;重发一个其实已经删掉的删除,回来的是「文件不存在」或 `130002`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;代码或参数名写错报 `100003`、缺必填参数报 `100001`)。**终态码 `999011`(凭证无效)/ `140002`(终态失败:异步生成失败、参数错误或业务处理失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,`140002` 立即重发得到的还是同一结果。
|
|
313
323
|
|
|
314
324
|
<!-- no-replay-endpoints
|
|
315
325
|
上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
|
|
@@ -378,6 +388,7 @@ vault.drive.delete-file
|
|
|
378
388
|
vault.drive.delete-folder
|
|
379
389
|
vault.drive.upload
|
|
380
390
|
vault.my-conference.download
|
|
391
|
+
vault.stock-note.add
|
|
381
392
|
vault.stock-pool.create
|
|
382
393
|
-->
|
|
383
394
|
- **Token 自愈**:服务端判定 Token 失效时,先换用本进程其他请求或其他进程已刷新好的 token,不行再重新登录一次,然后重发请求。换 token 的重发不占用网络重试次数;多个并发请求同时失效只登录一次。
|
|
@@ -414,6 +425,7 @@ vault.stock-pool.create
|
|
|
414
425
|
- `vault my-conference-list`
|
|
415
426
|
- `vault wechat-message-list`
|
|
416
427
|
- `vault wechat-chatroom-list`
|
|
428
|
+
- `vault stock-note-list`
|
|
417
429
|
- `ai hot-topic`
|
|
418
430
|
- `ai management-sentiment-indicator`
|
|
419
431
|
- `ai management-sentiment-conclusion`
|
|
@@ -426,7 +438,7 @@ vault.stock-pool.create
|
|
|
426
438
|
- `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
|
|
427
439
|
- 安全上限:自动翻页最多 1000 页,防止异常循环
|
|
428
440
|
- 部分页失败、或服务端实际返回行数与 `total` 矛盾(提前短页)时,不丢弃已取到的数据:结果带 `partial: true`(页失败时另有 `failedPages`;K线分片为 `failedShards`——早于账号可回溯窗口而跳过的分片不算失败、也不标 `partial`,另列在 `outOfWindowShards`;只有部分分片返回、合并时放不下的列为 `droppedColumns`;`quote` 系带 `--field` 而服务端没回的列为 `missingFields`;翻页时同一行在相邻两页各出现一次为 `duplicateRows`,按时间排序的列表在同一时刻的一组数据跨页时会发生,重复了几行也就漏了几行,重复行已去掉,缩短时间范围重拉可补齐;同一 ID 在后面的页以不同内容再次出现为 `changedRows`,说明翻页期间列表有变动、也可能漏了行,两版都保留(按 ID 去重只留一版),重拉即可——重拉后仍在同一处出现,说明这个列表里同一 ID 对应多条记录,按 ID 去重前先核对;`--format json` 可见),stderr 输出警告,**进程退出码为 3**(完整成功为 0)
|
|
429
|
-
- **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。这个分支很少触发——服务端对解析不了的代码直接报 `100003` 并点名是哪个(指标码拼错 →「指标 xxx 不存在」;证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O` →「xxx 不是有效证券或者板块ID」),**无论同批有没有正确的代码都会报**,CLI 相应退出 1。真实的无数据/无覆盖仍是占位单元格 + 退出码 0。占位值统一是 `null`。⚠️ **报告期类指标(`is_
|
|
441
|
+
- **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。这个分支很少触发——服务端对解析不了的代码直接报 `100003` 并点名是哪个(指标码拼错 →「指标 xxx 不存在」;证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O` →「xxx 不是有效证券或者板块ID」),**无论同批有没有正确的代码都会报**,CLI 相应退出 1。真实的无数据/无覆盖仍是占位单元格 + 退出码 0。占位值统一是 `null`。⚠️ **报告期类指标(`is_*`)与交易日类指标混查、或显式传 `--calendar-type ND` / `TD` / `WD` 时,时序上大部分行都是占位**(只有报告期末那几行是真值,`TD` 下落在非交易日的报告期末那几期整期缺失;单独查报告期类指标且不传 `--calendar-type` 时只返回报告期末),`null` 虽被 Excel / pandas / SQL 的聚合跳过,**但行数不变**,手工「总和 ÷ 行数」仍会差几十倍;详见 skill 的 `references/commands/indicator.md`。**条件选股的缺列另有更严的一档**:把缺列的变量当作无法求值,若表达式(按 `&&`/`||` 的布尔结构)再无任何可成立的分支,则**退出码 1 且不输出**——那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过。⚠️ 这一档以「服务端返回了命中行」为前提;**零命中时一律退出码 0**(没有行需要被质疑),所以空集不能直接当成「无标的符合条件」——另有两种成因产生**逐字相同**的输出:**日期没落在报告期末**(报告期类指标此时整批 `null`),或**该指标不覆盖这批证券**(如拿 A 股专属指标查港美股)。语义约定:`0` 完整成功(含合法空结果)/`3` 有数据但不完整/`4` 数据写出完整、但 `--output` 指向的文件在收尾期间被另一次写向同一路径的导出替换掉了(不是本次命令的产物,stderr 会说明并给出两边的 sha256 前缀;与 `3` 同时发生时退出 `3`)/`1` 硬失败/`130` / `143` / `129` 被 Ctrl-C / `kill` / 终端断开中断(进程以该信号结束,shell 里的 `$?` 即此值,脚本循环会随之停下;本次导出的暂存文件已删除;自动命名的下载被中断时可能留下一个 0 字节的同名文件,删掉即可;中断前已发出的请求服务端照常处理并计费)。接 `| head` 等管道提前关掉读端时,退出码同样保留,脚本需 `set -o pipefail` 才看得到
|
|
430
442
|
- **分页端点返回 `null` 也退出 3**:分页端点的正常响应是 `{total, list}`,真实的空结果是 `{total: 0, list: []}`。若响应体是 `null`,CLI 在 stderr 告警并**退出码 3**——只给告警的话,脚本无法区分「这个筛选确实没命中」和「这个筛选没生效」。机器格式(jsonl/csv)此时 **stdout 不输出任何字节**(不是空行),`--format json` 仍忠实打印 `null`。⚠️ 带 `--output` 时文件仍会被创建,为 0 字节(旁边的 `.meta.json` 标 `complete: false`)。
|
|
431
443
|
- 🔴 **`total` 被服务端封顶时会标 `totalCapped` 并退出 3**:分页端点的 `total` 若被服务端封顶(返回一个固定上限而非真实条数),省略 `--size` 的全量拉取会**正好取满那个上限就停、且不报任何异常**——导出的文件是截断的却看不出来。全量拉取结束后会**多探一行**(`from = total`):探到数据就标 `partial` + `totalCapped` 并退出 3;探针被服务端以偏移窗口类错误拒绝、或 `total` 正好等于端点声明的偏移窗口(`vault wechat-message-list` / `insight highlight list` 为 10000,窗口外的行取不到也数不到)时,同样按封顶处理。判据不写死 10000,服务端改配置仍然有效;`total` 诚实时探针返回空、不产生计费。传了 `--size` 的有界请求不做此探测。
|
|
432
444
|
- 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`);其他格式下 stderr 输出 `Total: N, showing: M`(json 格式不输出该行)
|
|
@@ -505,6 +517,9 @@ gangtise insight announcement download --announcement-id 123456 --file-type 2
|
|
|
505
517
|
gangtise insight research download --report-id 12345 --output ./report.pdf
|
|
506
518
|
|
|
507
519
|
gangtise insight roadshow list --institution C100000017
|
|
520
|
+
# 增量同步:昨天新收录、下周举办的路演(--publish-* 按收录时间 publishTime,--start/--end-time 按举办时间,两组同时生效;--publish-end-time 只写日期时按整天算)
|
|
521
|
+
gangtise insight roadshow list --publish-start-time 2026-10-29 --publish-end-time 2026-10-29 \
|
|
522
|
+
--start-time 2026-11-02 --end-time "2026-11-08 23:59:59" --format json
|
|
508
523
|
|
|
509
524
|
# 港股公告
|
|
510
525
|
gangtise insight announcement-hk list --security 01913.HK --rank-type 2 --size 20 --format json
|
|
@@ -618,6 +633,8 @@ gangtise quote realtime --security 512800.SH --security SPX.SPI --security HSI.H
|
|
|
618
633
|
gangtise quote realtime --security aShares --field securityCode --field latestPrice --field pctChange --field volume --format json
|
|
619
634
|
# A股个股日资金流向(沪深京;--security aShares 全市场;--limit 上限 10000,超限缩短日期区间分批)
|
|
620
635
|
gangtise quote fund-flow --security 600519.SH --security 000001.SZ --start-date 2026-06-01 --end-date 2026-06-05 --field mainNetInflow --field largeInflow --field xlargeInflow --format json
|
|
636
|
+
# 板块日资金流向(金额单位元,不点名字段时返回全部;板块 ID 用 reference sector-search 查)
|
|
637
|
+
gangtise quote sector-fund-flow --sector 1000001141 --sector 1000001065 --start-date 2026-10-26 --end-date 2026-10-30 --field sectorMainNetInflow --field sectorTotalInflow
|
|
621
638
|
```
|
|
622
639
|
|
|
623
640
|
> **历史 vs 实时**:`day-kline*` 仅返回历史数据(当日数据入库时间:A 股 ~15:30 / 港股 ~16:30 / 美股 ~07:00 北京时间)。盘中需要最新成交价、振幅等实时字段必须走 `quote realtime`。
|
|
@@ -735,7 +752,7 @@ gangtise fund etf-share-change --security 510300.SH --start-date 2026-09-01 --en
|
|
|
735
752
|
|
|
736
753
|
> **基金命令一律按次计费 0.4 积分**(与返回行数、基金只数无关,多只基金合并成一次调用最省),超时 / 5xx 不自动重放。不分页:单次上限 10000 行,超出整批报 `100006`,减少只数或缩短区间分批查。**代码不存在、不带后缀或后缀小写都返回空结果、不报错**——拿到空先核对代码。日期两端都不传 = 取账号可回溯窗口内的全部;起点早于窗口整批报 `110003`。
|
|
737
754
|
|
|
738
|
-
> **取数前注意**:各命令单位不同(`nav` / `asset-allocation` 为元,`asset-size` 为万份 / 万元,持仓明细为万股 / 万张 /
|
|
755
|
+
> **取数前注意**:各命令单位不同(`nav` / `asset-allocation` 为元,`asset-size` 为万份 / 万元,持仓明细为万股 / 万张 / 万元);持仓明细只返回证券简称、不返回代码;ETF 申赎成分的 `componentCode` 由 CLI 按名称匹配补上(匹配不到为 `null`,`--no-component-code` 关闭);`fund-type-allocation` 季报期只含重仓基金(`positionType: top`),中报 / 年报为全部持基(`all`)。逐条见 `gangtise-openapi/references/commands/fund.md`。
|
|
739
756
|
|
|
740
757
|
### AI
|
|
741
758
|
|
|
@@ -762,7 +779,7 @@ gangtise ai management-discuss-announcement --report-date 2025-12-31 --security-
|
|
|
762
779
|
gangtise ai management-discuss-announcement --report-date 2025-12-31 --security-code 000001.SZ --dimension all
|
|
763
780
|
# 管理层讨论-业绩会
|
|
764
781
|
gangtise ai management-discuss-earnings-call --report-date 2025-06-30 --security-code 000001.SZ --dimension financialPerformance
|
|
765
|
-
# 管理层情绪:指标与结论按条计费(5 积分/条),记得带 --size
|
|
782
|
+
# 管理层情绪:指标与结论按条计费(5 积分/条),记得带 --size;publishTime 与时间筛选都是北京时间
|
|
766
783
|
gangtise ai management-sentiment-indicator --security 300285.SZ --size 5
|
|
767
784
|
gangtise ai management-sentiment-conclusion --security 300285.SZ --start-time 2026-08-01 --end-time 2026-08-31 --size 5
|
|
768
785
|
# 逐条问答的评分与依据:--source-id 取上面返回的 source.sourceId(10 积分/次)
|
|
@@ -826,6 +843,9 @@ gangtise vault stock-pool-list
|
|
|
826
843
|
gangtise vault stock-pool-stocks --pool-id 808477293
|
|
827
844
|
# 查询所有股票池中的全量证券(默认行为)
|
|
828
845
|
gangtise vault stock-pool-stocks
|
|
846
|
+
# 所属租户的股票池:列表与明细都加 --space-type 2,明细的 --pool-id 要取自同一 --space-type 的列表
|
|
847
|
+
gangtise vault stock-pool-list --space-type 2
|
|
848
|
+
gangtise vault stock-pool-stocks --space-type 2 --pool-id 908477301
|
|
829
849
|
|
|
830
850
|
# 维护股票池:建池 → 批量关注 → 改名 → 取消关注 → 删池
|
|
831
851
|
# (这五个命令会改动账号数据;云盘的上传 / 新建 / 重命名 / 移动 / 复制 / 删除同样会,见上方 Vault 云盘管理)
|
|
@@ -835,6 +855,13 @@ gangtise vault stock-pool-rename --pool-id 808477293 --name "核心持仓"
|
|
|
835
855
|
gangtise vault stock-pool-remove-stock --pool-id 808477293 --security 000858.SZ
|
|
836
856
|
# 删池会连带移除池内全部关注关系且不可恢复,必须显式 --yes
|
|
837
857
|
gangtise vault stock-pool-delete --pool-id 808477293 --yes
|
|
858
|
+
|
|
859
|
+
# 个股投资笔记:查(不传筛选即全部笔记)→ 新增 → 改写 → 删除(四个命令都免费)
|
|
860
|
+
gangtise vault stock-note-list
|
|
861
|
+
gangtise vault stock-note-list --security 600519.SH --security 00700.HK
|
|
862
|
+
gangtise vault stock-note-add --security 688106.SH --security 600111.SH --content "上游材料涨价叠加国产替代,产业链同步受益。"
|
|
863
|
+
gangtise vault stock-note-update --note-id 1835 --content "电子特气国产替代逻辑清晰,最新季报订单加速落地。"
|
|
864
|
+
gangtise vault stock-note-delete --note-id 1835 --yes
|
|
838
865
|
```
|
|
839
866
|
|
|
840
867
|
> 云盘允许同名文件与文件夹(以 ID 区分),上传 / 新建 / 复制每执行一次就多一份,所以这三个命令超时不会自动重发;超时后重试前先用 `drive-folder-list` 看一眼是否已经存入。
|
package/dist/src/cli.js
CHANGED
|
@@ -4,19 +4,21 @@ import { ACCUMULATING_PARSERS, unknownChoiceMessage } from "./core/args.js";
|
|
|
4
4
|
import { ApiError, ValidationError } from "./core/errors.js";
|
|
5
5
|
import { removeStagingFiles } from "./core/output.js";
|
|
6
6
|
import { isVerbose, setVerbose } from "./core/transport.js";
|
|
7
|
-
import { checkForUpdate } from "./core/updateCheck.js";
|
|
7
|
+
import { checkForUpdate, noticeUpdate } from "./core/updateCheck.js";
|
|
8
8
|
import { CLI_VERSION } from "./version.js";
|
|
9
9
|
import { COMMAND_GROUPS } from "./commands/groups.js";
|
|
10
10
|
import { decidedExitCode, markFailed } from "./core/exitStatus.js";
|
|
11
11
|
const program = new Command();
|
|
12
|
+
let ranCommand;
|
|
12
13
|
program
|
|
13
14
|
.name("gangtise")
|
|
14
15
|
.description("Gangtise OpenAPI CLI")
|
|
15
16
|
.version(CLI_VERSION)
|
|
16
17
|
.option("--verbose", "Print per-request timings to stderr (also: GANGTISE_VERBOSE=1)")
|
|
17
|
-
.hook("preAction", (thisCommand) => {
|
|
18
|
+
.hook("preAction", (thisCommand, actionCommand) => {
|
|
18
19
|
if (thisCommand.opts().verbose)
|
|
19
20
|
setVerbose(true);
|
|
21
|
+
ranCommand = actionCommand;
|
|
20
22
|
});
|
|
21
23
|
for (const group of COMMAND_GROUPS) {
|
|
22
24
|
program.addCommand(group);
|
|
@@ -100,7 +102,7 @@ async function main() {
|
|
|
100
102
|
const firstArg = process.argv[2];
|
|
101
103
|
if (firstArg === "--version" || firstArg === "-V") {
|
|
102
104
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
103
|
-
if (process.stdout.isTTY) {
|
|
105
|
+
if (process.stdout.isTTY && !process.env.GANGTISE_NO_UPDATE_CHECK) {
|
|
104
106
|
await Promise.race([checkForUpdate(CLI_VERSION), new Promise((resolve) => setTimeout(resolve, 2500))]);
|
|
105
107
|
process.exit(0);
|
|
106
108
|
}
|
|
@@ -125,5 +127,9 @@ async function main() {
|
|
|
125
127
|
process.stderr.write("Unknown error\n");
|
|
126
128
|
markFailed();
|
|
127
129
|
}
|
|
130
|
+
finally {
|
|
131
|
+
if (process.stderr.isTTY && !process.env.CI && !process.env.GANGTISE_NO_UPDATE_CHECK && ranCommand?.name() !== "update")
|
|
132
|
+
noticeUpdate(CLI_VERSION);
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
void main();
|
package/dist/src/commands/ai.js
CHANGED
|
@@ -64,8 +64,8 @@ query(ai, "security-clue", {
|
|
|
64
64
|
from(), size(),
|
|
65
65
|
field(new Option("--start-time <datetime>", "Start time").argParser(datetimeArg("--start-time")).makeOptionMandatory(), (v) => ({ startTime: v })),
|
|
66
66
|
field(new Option("--end-time <datetime>", "End time").argParser(datetimeArg("--end-time")).makeOptionMandatory(), (v) => ({ endTime: v })),
|
|
67
|
-
field(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory(), (v) => ({ queryMode: v })),
|
|
68
|
-
list("--gts-code <code>", "
|
|
67
|
+
field(new Option("--query-mode <mode>", "bySecurity: --gts-code takes stock codes; byIndustry: Shenwan industry codes").choices(["bySecurity", "byIndustry"]).makeOptionMandatory(), (v) => ({ queryMode: v })),
|
|
68
|
+
list("--gts-code <code>", "Stock code (e.g. 600519.SH) or Shenwan industry code (e.g. 821035.SWI), per --query-mode; repeatable", "gtsCodeList"),
|
|
69
69
|
choiceList("--source <name>", "Source: researchReport/conference/announcement/view (repeat)", "source", ["researchReport", "conference", "announcement", "view"]),
|
|
70
70
|
format(), output(),
|
|
71
71
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { collectNames } from "../core/args.js";
|
|
3
|
-
import {
|
|
3
|
+
import { addComponentCodes, clientLookup } from "../core/etfComponents.js";
|
|
4
|
+
import { date, flag, format, list, output, query, requiredList, value } from "./shared.js";
|
|
4
5
|
export const fund = new Command("fund").description("Fund APIs: profiles, NAV, fees, managers, scale, holders, holdings and allocation, ETF creation/redemption");
|
|
5
6
|
const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund', keeping only rows whose category is fund", "fundCodeList");
|
|
6
7
|
const range = (what) => [
|
|
@@ -89,9 +90,10 @@ query(fund, "etf-pcf-header", {
|
|
|
89
90
|
fields: [code(), format(), output()],
|
|
90
91
|
});
|
|
91
92
|
query(fund, "etf-pcf-components", {
|
|
92
|
-
description: "ETF creation/redemption component list for the latest trade date: quantities, weights, cash substitution",
|
|
93
|
+
description: "ETF creation/redemption component list for the latest trade date: quantities, weights, cash substitution. componentCode is matched by the CLI from each component name",
|
|
93
94
|
endpoint: "fund.etf-pcf-components",
|
|
94
|
-
fields: [code(), format(), output()],
|
|
95
|
+
fields: [code(), format(), output(), flag("--no-component-code", "Skip matching componentCode: no download of the securities list, no name searches, no quote request")],
|
|
96
|
+
shape: (data, client, options) => options.componentCode === false ? Promise.resolve(data) : addComponentCodes(data, clientLookup(client)),
|
|
95
97
|
});
|
|
96
98
|
query(fund, "etf-share-change", {
|
|
97
99
|
description: "ETF shares and scale per trade date, with the change vs the previous one",
|
|
@@ -10,5 +10,6 @@ import { quote } from "./quote.js";
|
|
|
10
10
|
import { raw } from "./raw.js";
|
|
11
11
|
import { reference } from "./reference.js";
|
|
12
12
|
import { tool } from "./tool.js";
|
|
13
|
+
import { update } from "./update.js";
|
|
13
14
|
import { vault } from "./vault.js";
|
|
14
|
-
export const COMMAND_GROUPS = [auth, lookup, insight, quote, fundamental, bond, fund, reference, vault, ai, alternative, indicator, tool, raw];
|
|
15
|
+
export const COMMAND_GROUPS = [auth, lookup, insight, quote, fundamental, bond, fund, reference, vault, ai, alternative, indicator, tool, raw, update];
|
|
@@ -2,13 +2,16 @@ import { Command, Option } from "commander";
|
|
|
2
2
|
import { collectList, collectNames, dateArg, screenerExpressionFields, parseNumberOption, parseScreenerIndicators } from "../core/args.js";
|
|
3
3
|
import { buildIndicatorCrossSectionBody, buildIndicatorScreenerBody, buildIndicatorTimeSeriesBody } from "../core/commandBodies.js";
|
|
4
4
|
import { checkScreenerBindings, droppedFromMatrix, flattenCrossSection, flattenTimeSeries, isEmptyMatrix, requireIndicatorMatrix, unwrapIndicatorData } from "../core/indicatorMatrix.js";
|
|
5
|
-
import { resolveCalendarType } from "../core/calendarType.js";
|
|
6
5
|
import { ValidationError } from "../core/errors.js";
|
|
7
6
|
import { parseOutputFormat } from "../core/output.js";
|
|
8
7
|
import { printData } from "../core/printer.js";
|
|
9
8
|
import { withClient } from "./shared.js";
|
|
10
|
-
function flagDropped(rows, data, requestedSecurities, requestedIndicators) {
|
|
9
|
+
function flagDropped(rows, data, requestedSecurities, requestedIndicators, timeSeries = false) {
|
|
11
10
|
if (isEmptyMatrix(data)) {
|
|
11
|
+
if (timeSeries) {
|
|
12
|
+
process.stderr.write("[gangtise] note: the query returned no data at all. In a time series that usually means the range holds no date of its calendar: no trading day for a trading-day indicator (a holiday range), no report-period end (03-31 / 06-30 / 09-30 / 12-31) for a report-period one when --calendar-type is omitted or RD. Widen the range, or pass --calendar-type ND for one row per day. If the range does hold such dates, cross-check the codes against 'gangtise indicator search --format json' and 'gangtise reference securities-search'; if they are right, report it with the command.\n");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
12
15
|
process.stderr.write("[gangtise] note: the query returned no data at all. A missing value comes back as a null cell and an unrecognised code or parameter name as an error (100003), so an empty answer is neither. Cross-check the codes against 'gangtise indicator search --format json' and 'gangtise reference securities-search'; if they are right, report it with the command.\n");
|
|
13
16
|
return;
|
|
14
17
|
}
|
|
@@ -28,7 +31,7 @@ function flagDropped(rows, data, requestedSecurities, requestedIndicators) {
|
|
|
28
31
|
parts.push(`indicators ${indicators.join(", ")}`);
|
|
29
32
|
if (securities.length > 0)
|
|
30
33
|
parts.push(`securities ${securities.join(", ")}`);
|
|
31
|
-
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`);
|
|
34
|
+
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/.A, not .US). Result marked partial (exit 3).\n`);
|
|
32
35
|
}
|
|
33
36
|
function requireIndicatorScope(indicators, securities) {
|
|
34
37
|
const missing = [indicators.length === 0 ? "--indicator" : "", securities.length === 0 ? "--security" : ""].filter(Boolean);
|
|
@@ -74,7 +77,7 @@ indicator.command("time-series")
|
|
|
74
77
|
.option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, [])
|
|
75
78
|
.requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date"))
|
|
76
79
|
.requiredOption("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date"))
|
|
77
|
-
.option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday. Omit it and the
|
|
80
|
+
.option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday RD=report period (03-31/06-30/09-30/12-31). Omit it and the server picks by indicator: TD for trading-day indicators, RD for report-period ones, the union of both dates when they are mixed. Forecast indicators (frcst_*) get TD that way, though their values also change on non-trading days — pass ND to keep those")
|
|
78
81
|
.option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)")
|
|
79
82
|
.option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)")
|
|
80
83
|
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'", collectNames, [])
|
|
@@ -84,18 +87,10 @@ indicator.command("time-series")
|
|
|
84
87
|
.action((options) => withClient(async (client) => {
|
|
85
88
|
const format = parseOutputFormat(options.format);
|
|
86
89
|
requireIndicatorScope(options.indicator, options.security);
|
|
87
|
-
const
|
|
88
|
-
if (!body.calendarType) {
|
|
89
|
-
const resolved = await resolveCalendarType(client, options.indicator);
|
|
90
|
-
if (resolved) {
|
|
91
|
-
body.calendarType = resolved;
|
|
92
|
-
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`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const raw = await client.call("indicator.time-series", body);
|
|
90
|
+
const raw = await client.call("indicator.time-series", buildIndicatorTimeSeriesBody(options));
|
|
96
91
|
const data = requireIndicatorMatrix(raw);
|
|
97
92
|
const rows = flattenTimeSeries(data, options.keyBy, options.security);
|
|
98
|
-
flagDropped(rows, data, options.security, options.indicator);
|
|
93
|
+
flagDropped(rows, data, options.security, options.indicator, options.calendarType?.toUpperCase() !== "ND");
|
|
99
94
|
await printData(rows, format, options.output);
|
|
100
95
|
}));
|
|
101
96
|
indicator.command("screener")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { collectList, dateArg, maybeArray, parseChoiceList, parseEndTimestamp13, parseFrom, parseSize, parseTimestamp13 } from "../core/args.js";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { collectList, dateArg, datetimeArg, maybeArray, parseChoiceList, parseEndTimestamp13, parseFrom, parseSize, parseTimestamp13 } from "../core/args.js";
|
|
3
3
|
import { ValidationError } from "../core/errors.js";
|
|
4
4
|
import { fetchOpinionDetails } from "../core/opinionDetail.js";
|
|
5
5
|
import { rowCount } from "../core/rowSink.js";
|
|
6
|
-
import { emit, addDownloadCommand, confirmCostly, multiChoice, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
6
|
+
import { emit, addDownloadCommand, confirmCostly, multiChoice, field, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
7
7
|
export const insight = new Command("insight").description("Insight APIs");
|
|
8
8
|
const SW_INDUSTRY_ONLY = "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them";
|
|
9
9
|
const opinion = new Command("opinion");
|
|
@@ -84,6 +84,11 @@ query(pamirsSummary, "list", {
|
|
|
84
84
|
],
|
|
85
85
|
});
|
|
86
86
|
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" });
|
|
87
|
+
const publishTime = (flags, description, key) => {
|
|
88
|
+
const option = new Option(flags, description);
|
|
89
|
+
option.argParser(datetimeArg(option.long));
|
|
90
|
+
return field(option, (v) => ({ [key]: v }));
|
|
91
|
+
};
|
|
87
92
|
const addScheduleList = (command, endpointKey, fields) => query(command, "list", {
|
|
88
93
|
endpoint: endpointKey,
|
|
89
94
|
fields: [
|
|
@@ -99,6 +104,8 @@ const addScheduleList = (command, endpointKey, fields) => query(command, "list",
|
|
|
99
104
|
fields.location && list("--location <id>", "Location ID (domesticCity constant, via 'reference constant-list')", "locationList"),
|
|
100
105
|
format(), output("Output path"),
|
|
101
106
|
...timeFilters(),
|
|
107
|
+
publishTime("--publish-start-time <datetime>", "Collected from (publishTime, when the schedule first came in — not when it is held); combines with --start-time/--end-time", "publishStartTime"),
|
|
108
|
+
publishTime("--publish-end-time <datetime>", "Collected until (publishTime); a bare date covers that whole day", "publishEndTime"),
|
|
102
109
|
].filter((f) => Boolean(f)),
|
|
103
110
|
});
|
|
104
111
|
addScheduleList(roadshow, "insight.roadshow.list", {
|
|
@@ -179,3 +179,22 @@ quote.command("fund-flow")
|
|
|
179
179
|
await printData(data, format, options.output);
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
|
+
quote.command("sector-fund-flow")
|
|
183
|
+
.description("Sector daily fund flow: each sector's constituents on that day summed, SH/SZ/BJ listings only (free)")
|
|
184
|
+
.requiredOption("--sector <id>", "Sector ID from 'gangtise reference sector-search' (repeat or comma-separate)", collectList)
|
|
185
|
+
.option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)", dateArg("--start-date"))
|
|
186
|
+
.option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)", dateArg("--end-date"))
|
|
187
|
+
.option("--limit <number>", `Max rows returned (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). It cuts the answer only: a query too heavy to compute fails with 100006 whatever the limit — shorten the range, or pass fewer sectors or fields`)
|
|
188
|
+
.option("--field <field>", "Field, e.g. sectorMainNetInflow / sectorLargeInflow (repeat); omit for all. sectorId and tradeDate always come first", collectList, [])
|
|
189
|
+
.option("--format <format>", "Output format", "table")
|
|
190
|
+
.option("--output <path>")
|
|
191
|
+
.action((options) => emit(options, async (client) => {
|
|
192
|
+
const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }) ?? DEFAULT_QUOTE_LIMIT;
|
|
193
|
+
const fieldList = maybeArray(options.field);
|
|
194
|
+
const data = await client.call("quote.sector-fund-flow", { sectorList: options.sector, startDate: options.startDate, endDate: options.endDate, limit, fieldList });
|
|
195
|
+
if (fieldList?.every((field) => field === "sectorId" || field === "tradeDate") && rowCount(data) === 0) {
|
|
196
|
+
process.stderr.write(`[gangtise] note: no rows came back, and --field names no fund-flow column (${fieldList.join(", ")}). This endpoint answers such a request with an empty list rather than an error, so it says nothing about whether data exists: add a value field such as sectorMainNetInflow, or drop --field.\n`);
|
|
197
|
+
}
|
|
198
|
+
flagIfLimitTruncated(data, limit, "sector-fund-flow");
|
|
199
|
+
return data;
|
|
200
|
+
}));
|
|
@@ -221,6 +221,7 @@ export function query(parent, name, spec) {
|
|
|
221
221
|
return command.action(async (options) => {
|
|
222
222
|
const body = requestBody(fields, options);
|
|
223
223
|
const endpoint = typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint(options);
|
|
224
|
-
|
|
224
|
+
const shape = spec.shape;
|
|
225
|
+
return emit(options, shape ? async (client) => shape(await client.call(endpoint, body), client, options) : (client) => client.call(endpoint, body), spec.cache);
|
|
225
226
|
});
|
|
226
227
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { isVersionNewer } from "../core/args.js";
|
|
4
|
+
import { installVersion, refreshSkillCopies } from "../core/selfUpdate.js";
|
|
5
|
+
import { latestVersion, PACKAGE_NAME, REGISTRY } from "../core/updateCheck.js";
|
|
6
|
+
import { CLI_VERSION, PACKAGE_ROOT } from "../version.js";
|
|
7
|
+
export const update = new Command("update")
|
|
8
|
+
.description("Update to the latest published version (npm install -g), then refresh older copies of the Agent Skill installed for Claude Code / Codex / OpenClaw / Hermes")
|
|
9
|
+
.option("--check", "Only report whether a newer version is published; install nothing")
|
|
10
|
+
.option("--no-skill", "Leave the Agent Skill copies as they are")
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const latest = await latestVersion(undefined, 15_000, Date.now(), true);
|
|
13
|
+
if (!latest)
|
|
14
|
+
throw new Error(`Could not reach ${REGISTRY} to look up the latest version. Check the network and retry, or install directly: npm install -g ${PACKAGE_NAME}@latest --registry=${REGISTRY}`);
|
|
15
|
+
const outdated = isVersionNewer(latest, CLI_VERSION);
|
|
16
|
+
if (options.check) {
|
|
17
|
+
process.stdout.write(outdated ? `Update available: ${CLI_VERSION} → ${latest}. Run: gangtise update\n` : `gangtise ${CLI_VERSION} is the latest version.\n`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (outdated) {
|
|
21
|
+
installVersion(latest, PACKAGE_ROOT);
|
|
22
|
+
process.stdout.write(`Updated gangtise ${CLI_VERSION} → ${latest}.\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.stdout.write(`gangtise ${CLI_VERSION} is the latest version.\n`);
|
|
26
|
+
}
|
|
27
|
+
if (options.skill)
|
|
28
|
+
for (const line of refreshSkillCopies(PACKAGE_ROOT, os.homedir()))
|
|
29
|
+
process.stdout.write(`${line}\n`);
|
|
30
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { collectList, collectNames } from "../core/args.js";
|
|
2
|
+
import { collectList, collectNames, parseFrom, parseSize } from "../core/args.js";
|
|
3
3
|
import { uploadDriveFile } from "../core/driveUpload.js";
|
|
4
4
|
import { ValidationError } from "../core/errors.js";
|
|
5
5
|
import { flagFailedItems } from "../core/normalize.js";
|
|
6
|
-
import { emit, addDownloadCommand, assertConfirmed, field, value, required, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
6
|
+
import { emit, addDownloadCommand, assertConfirmed, field, value, required, requiredList, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
7
7
|
export const vault = new Command("vault").description("Vault APIs");
|
|
8
8
|
query(vault, "drive-list", {
|
|
9
9
|
endpoint: "vault.drive.list",
|
|
@@ -163,11 +163,13 @@ query(vault, "wechat-chatroom-list", {
|
|
|
163
163
|
format(), output(),
|
|
164
164
|
],
|
|
165
165
|
});
|
|
166
|
-
|
|
166
|
+
const poolSpaceType = () => field(new Option("--space-type <n>", "Pools: 1=my pools (default) 2=tenant pools").choices(["1", "2"]), (v) => ({ spaceType: v === undefined ? undefined : Number(v) }));
|
|
167
|
+
query(vault, "stock-pool-list", { endpoint: "vault.stock-pool.list", fields: [poolSpaceType(), format(), output()] });
|
|
167
168
|
query(vault, "stock-pool-stocks", {
|
|
168
169
|
endpoint: "vault.stock-pool.stocks",
|
|
169
170
|
fields: [
|
|
170
|
-
|
|
171
|
+
poolSpaceType(),
|
|
172
|
+
field(new Option("--pool-id <id>", "Pool ID from 'stock-pool-list' with the same --space-type (an ID of the other space is not matched); repeat for multiple; omit (or 'all') for all pools of that space").argParser(collectList), (v) => ({ poolIdList: v?.length ? v : ["all"] })),
|
|
171
173
|
format(), output(),
|
|
172
174
|
],
|
|
173
175
|
});
|
|
@@ -219,3 +221,46 @@ vault.command("stock-pool-delete")
|
|
|
219
221
|
return data;
|
|
220
222
|
});
|
|
221
223
|
});
|
|
224
|
+
vault.command("stock-note-list").description("List your stock notes: all, by security, or by note ID (free)")
|
|
225
|
+
.option("--security <code>", "Security code (repeat or comma-separate): notes linked to ANY of them; omit both filters for all your notes", collectList)
|
|
226
|
+
.option("--note-id <id>", "Note ID (repeat or comma-separate); not together with --security", collectList)
|
|
227
|
+
.option("--from <number>", "Starting offset", "0")
|
|
228
|
+
.option("--size <number>", "Total rows to return; omit to fetch all")
|
|
229
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
230
|
+
.action((options) => {
|
|
231
|
+
if (options.security?.length && options.noteId?.length)
|
|
232
|
+
throw new ValidationError("pass --security or --note-id, not both");
|
|
233
|
+
const filter = options.noteId?.length ? { stockNoteIdList: options.noteId } : { securityList: options.security?.length ? options.security : ["all"] };
|
|
234
|
+
const body = { ...filter, from: parseFrom(options.from), size: parseSize(options.size) };
|
|
235
|
+
return emit(options, (client) => client.call("vault.stock-note.list", body));
|
|
236
|
+
});
|
|
237
|
+
query(vault, "stock-note-add", {
|
|
238
|
+
description: "Add a stock note linked to 1-50 securities (free). Not retried on a timeout: check 'stock-note-list' before re-running, or you get two notes",
|
|
239
|
+
endpoint: "vault.stock-note.add",
|
|
240
|
+
fields: [
|
|
241
|
+
requiredList("--security <code>", "Security code to link, 1-50 (repeat or comma-separate); fixed once created. Stocks, ETFs, exchange / concept / CITIC industry / global indices; .SWI is refused (120001)", "securityList"),
|
|
242
|
+
required("--content <text>", "Note text, plain (Markdown is kept as text), max 10000 characters", "content"),
|
|
243
|
+
format("json"), output(),
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
query(vault, "stock-note-update", {
|
|
247
|
+
description: "Replace the whole content of a stock note (free); its securities cannot be changed",
|
|
248
|
+
endpoint: "vault.stock-note.update",
|
|
249
|
+
fields: [
|
|
250
|
+
required("--note-id <id>", "Note ID from 'stock-note-list'", "stockNoteId"),
|
|
251
|
+
required("--content <text>", "New text, replacing the old in full; max 10000 characters", "content"),
|
|
252
|
+
format("json"), output(),
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
vault.command("stock-note-delete").description("Delete stock notes — irreversible (free)")
|
|
256
|
+
.requiredOption("--note-id <id>", "Note ID to delete; repeat or comma-separate, max 100", collectList)
|
|
257
|
+
.option("--yes", "Required: confirm the deletion")
|
|
258
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
259
|
+
.action(async (options) => {
|
|
260
|
+
assertConfirmed("vault.stock-note.delete", Boolean(options.yes), options.noteId.join("、"));
|
|
261
|
+
await emit(options, async (client) => {
|
|
262
|
+
const data = await client.call("vault.stock-note.delete", { stockNoteIdList: options.noteId });
|
|
263
|
+
flagFailedItems(data, "vault stock-note-delete");
|
|
264
|
+
return data;
|
|
265
|
+
});
|
|
266
|
+
});
|