gangtise-openapi-cli 0.18.0 → 0.20.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 +140 -133
- package/dist/src/cli.js +67 -10
- package/dist/src/core/args.js +26 -0
- package/dist/src/core/auth.js +23 -1
- package/dist/src/core/client.js +44 -12
- package/dist/src/core/commandBodies.js +28 -1
- package/dist/src/core/download.js +10 -2
- package/dist/src/core/endpoints.js +72 -0
- package/dist/src/core/indicatorMatrix.js +94 -0
- package/dist/src/core/output.js +4 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,59 @@
|
|
|
1
1
|
# Gangtise OpenAPI CLI
|
|
2
2
|
|
|
3
|
-
一个可直接调用 Gangtise OpenAPI
|
|
3
|
+
一个可直接调用 Gangtise OpenAPI 获取全量金融信息的命令行工具,同时提供Agent Skill。
|
|
4
4
|
|
|
5
5
|
## Changelog
|
|
6
6
|
|
|
7
|
+
### v0.20.0 — 2026-06-26
|
|
8
|
+
|
|
9
|
+
**新增接口**
|
|
10
|
+
- `insight announcement-us list` / `download` — 美股公司公告列表与下载(`--security TSLA.O`、`--category`〔分类用 `reference constant-list --category usShareAnnouncementCategory`,美股独立的 `103980xxx` 段〕、`--search-type`、`--rank-type`、下载 `--file-type 1` 原始 PDF / `2` Markdown);自动翻页,单页上限 50
|
|
11
|
+
- `ai stock-summary` — 个股看点(精炼投研总结):`--security` 传具体代码(A股/港股,可重复,单次最多 6000)或市场关键词 `aShares` / `hkStocks` 拉全市场;无看点的证券不返回、不扣分
|
|
12
|
+
- `fundamental income-statement-us` / `balance-sheet-us` / `cash-flow-us` — 美股三大财务报表(参数同其他财报:`--security-code` / `--period` / `--report-type` / `--fiscal-year` / `--field` 等)
|
|
13
|
+
- `reference chiefs-search` — 首席分析师 ID 搜索(`--keyword` 按姓名/机构/团队匹配,`--top` 默认 10);用于 `insight opinion list --chief` 的入参
|
|
14
|
+
|
|
15
|
+
**变更**
|
|
16
|
+
- `insight announcement-hk download` 新增 `--file-type`(`1` 原始(默认)/ `2` Markdown),此前无格式选项
|
|
17
|
+
|
|
18
|
+
**行为变更(注意)**
|
|
19
|
+
- ⚠️ `auth login` / `auth status` 默认脱敏 access token:`--format json` 输出里 `authorization` 与 `cache.accessToken` 显示为 `<redacted>`,仅保留过期时间 / 用户名 / 产品码 / uid 等非敏感字段。**依赖 `auth login` 原始 token 输出的脚本会拿到 `<redacted>`**,需改用 `auth login --show-token` 获取明文。
|
|
20
|
+
|
|
21
|
+
**修复(安全)**
|
|
22
|
+
- `auth status` / `auth login` token 脱敏:按凭证字段名模式匹配(`token`/`key`/`secret`/`password`/`credential`),覆盖 `apiKey`/`privateKey`/`refreshToken` 等任何可能携带的凭证字段
|
|
23
|
+
- 自愈守卫:同时设 `GANGTISE_TOKEN` + AK/SK 时,注入 token 失效后重新登录不再被旧 token 短路,重试改用登录拿到的新 token
|
|
24
|
+
|
|
25
|
+
**修复(数据正确性 / 健壮性)**
|
|
26
|
+
- ⚠️ **CSV 负数不再被破坏**(影响所有 CSV 导出):此前防公式注入会把负数(如跌幅 `-3.5`)加 `'` 前缀变成文本,Excel/pandas 无法参与计算;现仅对非有限数字的可疑串(`=`/`@`/`-1+cmd` 等)加前缀,合法数字原样输出
|
|
27
|
+
- 自动翻页改为 fail-soft:某页遇不可重试错误(限流 `903301` 等)不再丢弃已取的全部数据,返回已取页 + `partial` / `failedPages` 标记,并在首错后停止继续请求(避免撞限流多烧配额)
|
|
28
|
+
- 下载文件名 fallback(服务端 `Content-Disposition`)补清洗:含 `/`、`:` 等字符的文件名不再写到意外路径
|
|
29
|
+
- `ai stock-summary` / `ai knowledge-batch` 缺 `--security` / `--query` 时本地报错,不再发空请求(stock-summary 借此避免被后台当全市场误扣积分)
|
|
30
|
+
- `ai hot-topic` `--no-with-related-securities` / `--no-with-close-reading` 改为显式发 `false`(语义更明确,不依赖"字段缺失=排除"的隐含约定)
|
|
31
|
+
|
|
32
|
+
**修复(indicator 适配 EDE 后台新结构)**
|
|
33
|
+
- `indicator cross-section` / `time-series` 适配后台改版的返回结构(字段名加 `List` 后缀 `securityCodeList/indicatorCodeList/…`、截面 `values` 改二维 `[指标][证券]`):此前后台改结构后 CLI 拍平失配、退化成原始矩阵,现恢复 `{date, security, name, 指标:值}` 宽表。配合后台同步变化——无数据从 `999999` 报错改为返回 `null`(截面不再 500、不丢行),缺必填参数从笼统 `410106` 改为直接指明缺哪个参数
|
|
34
|
+
|
|
35
|
+
### v0.19.0 — 2026-06-24
|
|
36
|
+
|
|
37
|
+
**新增接口(Indicator · 证券级数据指标 EDE)**
|
|
38
|
+
- `indicator search` — 按名称搜索证券级数据指标,返回 `indicatorCode` 及可传参数 `parameterList`(含 `required` 必填标记与枚举);取数前必先 search 拿 code,绝不猜编码
|
|
39
|
+
- `indicator cross-section` — 指标截面数据(多指标 × 多证券,单日快照):`--indicator` / `--security`(均可重复)/ `--date` / `--currency` / `--scale` / `--indicator-param`
|
|
40
|
+
- `indicator time-series` — 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间):另有 `--start-date` / `--end-date` / `--calendar-type`(`ND`/`TD`/`WD`)
|
|
41
|
+
- 复权等指标专属参数用 `--indicator-param "code:key=value"`,参数 key 与取值以 search 的 `parameterList` 为准(行情复权键为 `adjustmentType`:`1` 不复权 / `2` 前复权 / `3` 后复权)
|
|
42
|
+
- 很多指标有必填参数,默认调用会报 `410106`(缺必填参数):N 期统计补 `periodNum`、区间/周期类补 `startDate`、年度/分红类补 `fiscalYear`;`999999` 多为「该证券公司类型/报告期无数据」而非系统故障。详见 `gangtise-openapi/references/commands/indicator.md`
|
|
43
|
+
|
|
44
|
+
**修复**
|
|
45
|
+
- `vault stock-pool-stocks --pool-id <id>` 过滤失效:此前因选项默认值 `["all"]` 泄漏,传具体 pool id 仍返回全部股票池证券;现已修复——传 id 精确过滤,省略则默认全量
|
|
46
|
+
- `auth` 缺凭证报错补充跨 shell(bash/zsh/fish)的 `export` 提示
|
|
47
|
+
|
|
48
|
+
**文档**
|
|
49
|
+
- README / SKILL 补充 indicator 命令组与取数最佳实践;`official-account` 命令文档补全
|
|
50
|
+
|
|
51
|
+
### v0.18.0 — 2026-06-17
|
|
52
|
+
|
|
53
|
+
**新增接口(Insight · 产业公众号资讯)**
|
|
54
|
+
- `insight official-account list` — 查询公众号资讯列表:支持 `--keyword`(需用数据中的具体词,非整句白话)/ `--account-id`(公众号 ID)/ `--security` / `--category`(文章类型枚举:`news`/`law`/`report`/`view`/`data`/`event`/`meeting`/`notice`/`recruit`/`investEdu`/`brand`/`notes`/`other`)/ `--industry`(`citicIndustry`/`swIndustry` 行业 ID)/ `--search-type`(`1` 标题 / `2` 全文)/ `--rank-type`(`1` 综合 / `2` 时间倒序);返回含模型生成摘要 `summary` 及关联行业/题材/证券列表
|
|
55
|
+
- `insight official-account download --article-id <id>` — 下载公众号文章:`--file-type 1` txt(默认)/ `2` HTML
|
|
56
|
+
|
|
7
57
|
### v0.17.0 — 2026-06-15
|
|
8
58
|
|
|
9
59
|
**接口变更(Breaking)**
|
|
@@ -28,106 +78,7 @@
|
|
|
28
78
|
- `lookup` 仅保留 2 个 API 未覆盖的本地表:`broker-org` / `meeting-org`
|
|
29
79
|
- 路演/调研/策略会/论坛 list 新增 `--location <id>` 按城市过滤(domesticCity 常量 ID;服务端过滤 v0.17.0 起已生效)
|
|
30
80
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
**新增接口**
|
|
34
|
-
- `alternative concept-info` — 题材指数基本信息:返回题材整体画像(定义 / 投资逻辑 / 行业空间 / 竞争格局 / 催化事件)。按 `--concept-id` 查询,仅返回最新截面数据,不支持历史回溯
|
|
35
|
-
- `alternative concept-securities` — 题材指数成分股(题材深度 F8):按分组结构返回当前成分股,每只含是否重点个股 `isKey` 与纳入理由 `inclusionReason`。按 `--concept-id` 查询
|
|
36
|
-
|
|
37
|
-
**接口变更**
|
|
38
|
-
- `quote index-day-kline` 返回字段新增 `securityName`(指数名称,如"上证指数")
|
|
39
|
-
|
|
40
|
-
> `--concept-id` 与主题跟踪 `ai theme-tracking --theme-id` 共用同一套题材 ID 体系,可用 `gangtise lookup theme-id list` 按名称查询(如 机器人 → `121000130`)。
|
|
41
|
-
|
|
42
|
-
### v0.14.4 — 2026-05-29
|
|
43
|
-
|
|
44
|
-
**Bug fix(全市场 K 线分片容错)**
|
|
45
|
-
- `quote day-kline --security all` 等全市场查询的日期分片改为容错:部分分片失败时返回已成功分片的数据并标记 `partial: true` + `failedShards`(失败的日期区间),同时向 stderr 告警;只有全部分片失败才抛错。此前为 fail-fast,单片失败会让整次查询失败,或在异常路径上被误判为空结果。
|
|
46
|
-
|
|
47
|
-
### v0.14.3 — 2026-05-29
|
|
48
|
-
|
|
49
|
-
**性能 / 健壮性**
|
|
50
|
-
- 标题缓存按端点封顶(5000 条/端点)并清理过期项,修复 `title-cache.json` 无上限增长(曾达 ~58MB)拖慢启动的问题
|
|
51
|
-
- 下载接口遇鉴权失效(`8000014` / `8000015`)自动刷新 token 并重试一次(此前仅普通 JSON 调用具备 token 自愈)
|
|
52
|
-
- CLI handler 抽出 `emit` / `withClient` 公共封装去除重复样板;CSV 转义逻辑去重;翻页与 K 线分片统一走 `GANGTISE_PAGE_CONCURRENCY` 并发控制
|
|
53
|
-
- 补齐多个 core 模块的单元测试
|
|
54
|
-
|
|
55
|
-
### v0.14.2 — 2026-05-22
|
|
56
|
-
|
|
57
|
-
**Bug fix(A 股 / HK 全市场 K 线同源问题)**
|
|
58
|
-
- `quote day-kline --security all` 由 2 天/片改为 **1 天/片**(A 股全市场单日约 5500 行)
|
|
59
|
-
- `quote day-kline-hk --security all` 由 3 天/片改为 **2 天/片**(港股全市场单日约 2770 行)
|
|
60
|
-
- 根治性修复:`callKlineWithSharding` 在 `--security all` 路径上,若用户未显式传 `--limit`,强制写入 `limit: 10000`(API 上限),不再走默认 6000——这样即便分片日数估算偏大,每个 shard 也能拿满 10K 行。用户自己传的 `--limit` 仍然保留生效。
|
|
61
|
-
|
|
62
|
-
### v0.14.1 — 2026-05-22
|
|
63
|
-
|
|
64
|
-
**Bug fix**
|
|
65
|
-
- `quote day-kline-us --security all` 分片由 2 天/片改为 **1 天/片**。美股全市场单日约 5800 行,原 2 天/片会在第一个 shard 命中默认 `--limit 6000` 上限,导致 shard 内第二日数据被截断到几百行。改 1 天/片后每个 shard 数据完整。
|
|
66
|
-
|
|
67
|
-
### v0.14.0 — 2026-05-22
|
|
68
|
-
|
|
69
|
-
**新增接口**
|
|
70
|
-
- `quote realtime` — 个股实时行情快照,单接口同时覆盖 A 股 / 港股 / 美股;支持代码混合传入或市场关键字(`aShares` / `hkStocks` / `usStocks`)批量查询全市场
|
|
71
|
-
- `quote day-kline-us` — 美股历史日 K 线,数据范围 NYSE / NASDAQ / AMEX;支持 `--security all` 全市场(CLI 自动按 1 天/片切分并发拉取,美股全市场单日约 5800 行)
|
|
72
|
-
|
|
73
|
-
**接口变更**
|
|
74
|
-
- `quote day-kline` / `quote day-kline-hk` 明确仅返回**历史**日 K 线,不包含盘中实时数据;当日数据入库时间:A 股 ~15:30 / 港股 ~16:30(北京时间)。盘中实时请走 `quote realtime`
|
|
75
|
-
- `fundamental valuation-analysis` 返回字段移除 `p10` / `p25` / `p75` / `p90`(仍保留 `value` / `percentileRank` / `average` / `median` / `upper1Std` / `lower1Std`)
|
|
76
|
-
|
|
77
|
-
### v0.13.0 — 2026-05-15
|
|
78
|
-
|
|
79
|
-
**新增接口**
|
|
80
|
-
- `fundamental income-statement-hk / balance-sheet-hk / cash-flow-hk` — 港股三大报表(中国会计准则)
|
|
81
|
-
- `alternative edb-search` — 行业指标列表搜索(按关键词匹配指标名称,返回 indicatorId 等元信息)
|
|
82
|
-
- `alternative edb-data` — 行业指标时序数据(批量按 indicatorId 拉取时间序列,最多 10 个指标)
|
|
83
|
-
- `vault stock-pool-list` — 查询用户自选股股票池列表(poolId / poolName)
|
|
84
|
-
- `vault stock-pool-stocks` — 查询股票池证券明细(支持 `--pool-id all` 全量查询)
|
|
85
|
-
|
|
86
|
-
**接口变更**
|
|
87
|
-
- `fundamental income-statement / balance-sheet / cash-flow / income-statement-quarterly / cash-flow-quarterly` 名称调整为 A股报表(路径不变)
|
|
88
|
-
- `ai management-discuss-announcement` `--dimension` 新增 `all` 选项,返回报告中完整的管理层讨论内容(内容可能较长)
|
|
89
|
-
- `vault wechat-message-list` 新增 `--security <code>` 参数(按证券代码过滤),返回结果增加 `securityList` 字段
|
|
90
|
-
|
|
91
|
-
### v0.12.0 — 2026-05-10
|
|
92
|
-
|
|
93
|
-
**性能 / 架构**
|
|
94
|
-
- 翻页并行化:自动翻页接口拉到首页 `total` 后,剩余页通过 `Promise.all` 并发请求(默认并发 5,`GANGTISE_PAGE_CONCURRENCY` 可调)
|
|
95
|
-
- 共享 `undici.Agent`:所有请求复用连接池(keep-alive 60s,max 16 连接),避免重复 TLS 握手
|
|
96
|
-
- 流式下载:`--output` 指定时二进制响应直接 `pipeline` 到磁盘,不再走内存 `Uint8Array`
|
|
97
|
-
- 流式输出:`--format jsonl/csv --output xxx` 且 ≥1000 行时逐行写盘
|
|
98
|
-
- Token 内存缓存:Token 在进程内不再每次读盘
|
|
99
|
-
- 自动重试:5xx / `ECONNRESET` / `ETIMEDOUT` / `999999` 自动指数退避重试 2 次
|
|
100
|
-
- Token 自愈:8000014/8000015 自动重新登录并重试一次
|
|
101
|
-
- 异步轮询退避:`earnings-review` / `viewpoint-debate` 轮询从固定 15s 改为 5→8→13→20→30s 指数退避
|
|
102
|
-
- K线自动分片:`quote day-kline --security all` 等全市场查询自动按日期切分并发执行
|
|
103
|
-
- 标题缓存:原"读全文→改→写全文"改为内存快照 + 原子写入(temp+rename)
|
|
104
|
-
|
|
105
|
-
**调试 / 可观测性**
|
|
106
|
-
- 新增 `--verbose` / `GANGTISE_VERBOSE=1`:打印每个请求的耗时、状态码、响应字节数到 stderr
|
|
107
|
-
|
|
108
|
-
### v0.11.1 — 2026-05-10
|
|
109
|
-
|
|
110
|
-
**新增接口**
|
|
111
|
-
- `insight announcement-hk list/download` — 查询/下载港股公告
|
|
112
|
-
- `insight foreign-opinion list` — 查询外资机构观点(外资券商)
|
|
113
|
-
- `insight independent-opinion list/download` — 查询/下载外资独立分析师观点
|
|
114
|
-
- `reference securities-search` — GTS Code 搜索(按名称/代码/拼音多维度匹配证券)
|
|
115
|
-
|
|
116
|
-
**接口变更**
|
|
117
|
-
- `insight summary download` 新增可选 `--file-type`(`1`=原始内容 / `2`=HTML),仅影响来源为会议平台的纪要
|
|
118
|
-
- `insight announcement list/download` 名称调整为"查询A股公告列表/下载A股公告文件"(路径不变)
|
|
119
|
-
- `insight opinion list` 名称调整为"查询内资机构观点列表"(路径不变)
|
|
120
|
-
|
|
121
|
-
### v0.11.0 — 2026-04-17
|
|
122
|
-
|
|
123
|
-
- 新增 `ai viewpoint-debate` / `viewpoint-debate-check` — 观点PK(异步)
|
|
124
|
-
- 新增 `ai management-discuss-announcement` / `management-discuss-earnings-call` — 管理层讨论
|
|
125
|
-
|
|
126
|
-
### v0.10.9 — 2026-04-10
|
|
127
|
-
|
|
128
|
-
- 修复信封检测、版本更新检查、端点去重
|
|
129
|
-
- 新增 `quote index-day-kline` 指数日K线
|
|
130
|
-
- 新增 `vault wechat-message-list` / `wechat-chatroom-list` 群消息
|
|
81
|
+
> 更早版本(v0.15.0 及之前)的完整更新历史见 [GitHub Releases](https://github.com/gangtiser/gangtise-openapi-cli/releases)。
|
|
131
82
|
|
|
132
83
|
## 首次安装
|
|
133
84
|
|
|
@@ -141,6 +92,12 @@ npm install -g gangtise-openapi-cli
|
|
|
141
92
|
gangtise --help
|
|
142
93
|
```
|
|
143
94
|
|
|
95
|
+
更新到最新版(`gangtise --version` 会自动与线上版本比对):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm update -g gangtise-openapi-cli
|
|
99
|
+
```
|
|
100
|
+
|
|
144
101
|
本地开发:
|
|
145
102
|
|
|
146
103
|
```bash
|
|
@@ -150,36 +107,6 @@ npm install
|
|
|
150
107
|
npm run dev -- --help
|
|
151
108
|
```
|
|
152
109
|
|
|
153
|
-
## 发布
|
|
154
|
-
|
|
155
|
-
npm 发版通过 GitHub Actions Trusted Publishing 完成,不需要 `NPM_TOKEN`。npm 包设置里的 Trusted Publisher 需要匹配本仓库和 workflow 文件名 `publish.yml`。
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
npm version patch --no-git-tag-version
|
|
159
|
-
npm run prepare
|
|
160
|
-
VERSION=$(node -p "require('./package.json').version")
|
|
161
|
-
git commit -am "chore: release v$VERSION"
|
|
162
|
-
git tag -a "v$VERSION" -m "v$VERSION" # 必须 annotated:--follow-tags 不推 lightweight tag
|
|
163
|
-
git push --follow-tags
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
推送 `v*` tag 后,`.github/workflows/publish.yml` 会在 GitHub-hosted runner 上使用 OIDC 发布到 `https://registry.npmjs.org/`。也可以从 GitHub Actions 页面手动运行该 workflow。
|
|
167
|
-
|
|
168
|
-
## 版本更新
|
|
169
|
-
|
|
170
|
-
查看当前版本(自动与线上版本比对):
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
gangtise --version
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
手动更新到最新版:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
npm update -g gangtise-openapi-cli
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
|
|
183
110
|
## 环境配置
|
|
184
111
|
|
|
185
112
|
优先读取以下环境变量:
|
|
@@ -217,6 +144,7 @@ gangtise-openapi/
|
|
|
217
144
|
│ ├── ai.md # AI 能力命令(one-pager / earnings-review / viewpoint-debate 等)
|
|
218
145
|
│ ├── alternative.md # 行业指标数据库(EDB search / EDB data)
|
|
219
146
|
│ ├── fundamental.md # 财务数据命令(A股/港股三大报表 / 估值 / 盈利预测 / 股东)
|
|
147
|
+
│ ├── indicator.md # 证券级数据指标 EDE(search / 截面 / 时序)
|
|
220
148
|
│ ├── insight.md # 投研内容命令(研报 / 观点 / 纪要 / 公告 / 外资)
|
|
221
149
|
│ ├── quote.md # 行情命令(A股/港股/指数 K 线)
|
|
222
150
|
│ ├── reference-and-lookup.md # GTS Code 搜索与枚举速查
|
|
@@ -269,10 +197,13 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
269
197
|
| | `research list` / `download` | 研报(含 Markdown 下载) |
|
|
270
198
|
| | `foreign-report list` / `download` | 外资研报(含中文翻译下载) |
|
|
271
199
|
| | `announcement list` / `download` | A股公告(含 Markdown 下载) |
|
|
272
|
-
| | `announcement-hk list` / `download` |
|
|
200
|
+
| | `announcement-hk list` / `download` | 港股公告(含 PDF/Markdown 下载) |
|
|
201
|
+
| | `announcement-us list` / `download` | 美股公告(含 PDF/Markdown 下载) |
|
|
273
202
|
| | `foreign-opinion list` | 外资机构观点 |
|
|
274
203
|
| | `independent-opinion list` / `download` | 外资独立分析师观点(含原文/翻译HTML下载) |
|
|
204
|
+
| | `official-account list` / `download` | 产业公众号资讯(含 txt/HTML 下载) |
|
|
275
205
|
| **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
|
|
206
|
+
| | `chiefs-search` | 首席分析师 ID 搜索(按姓名/机构/团队匹配) |
|
|
276
207
|
| | `constant-category` | 常量分类列表(含各分类适用的接口与参数) |
|
|
277
208
|
| | `constant-list` | 按分类导出常量值全量列表(行业/城市/公告分类/区域等) |
|
|
278
209
|
| | `concept-search` | 题材 ID 搜索(名称/拼音/分组名匹配) |
|
|
@@ -285,6 +216,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
285
216
|
| **Fundamental** | `income-statement` / `balance-sheet` / `cash-flow` | A股三大财务报表(累计) |
|
|
286
217
|
| | `income-statement-quarterly` / `cash-flow-quarterly` | A股利润表/现金流量表(单季度) |
|
|
287
218
|
| | `income-statement-hk` / `balance-sheet-hk` / `cash-flow-hk` | 港股三大财务报表(中国会计准则) |
|
|
219
|
+
| | `income-statement-us` / `balance-sheet-us` / `cash-flow-us` | 美股三大财务报表 |
|
|
288
220
|
| | `main-business` | 主营构成(按地区/产品拆分) |
|
|
289
221
|
| | `valuation-analysis` | 估值分析 |
|
|
290
222
|
| | `earning-forecast` | 盈利预测(一致预期) |
|
|
@@ -292,6 +224,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
292
224
|
| **AI** | `knowledge-batch` | 知识库批量检索 |
|
|
293
225
|
| | `knowledge-resource-download` | 知识资源下载 |
|
|
294
226
|
| | `security-clue` | 个股线索 |
|
|
227
|
+
| | `stock-summary` | 个股看点(精炼投研总结,按代码或全市场;仅 A 股/港股) |
|
|
295
228
|
| | `one-pager` | 一页通 |
|
|
296
229
|
| | `investment-logic` | 投资逻辑 |
|
|
297
230
|
| | `peer-comparison` | 同业对比 |
|
|
@@ -307,6 +240,9 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
307
240
|
| | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
|
|
308
241
|
| | `wechat-message-list` / `wechat-chatroom-list` | 群消息列表与群ID查询 |
|
|
309
242
|
| | `stock-pool-list` / `stock-pool-stocks` | 自选股股票池列表与证券明细 |
|
|
243
|
+
| **Indicator** | `search` | 证券级数据指标搜索(按名称匹配,返回 indicatorCode 及可传参数 parameterList) |
|
|
244
|
+
| | `cross-section` | 指标截面数据(多指标 × 多证券,单日快照;前置 `search` 拿 code) |
|
|
245
|
+
| | `time-series` | 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间) |
|
|
310
246
|
| **Alternative** | `edb-search` | 行业指标搜索(按关键词匹配,返回 indicatorId 等元信息) |
|
|
311
247
|
| | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标) |
|
|
312
248
|
| | `concept-info` | 题材指数基本信息(投资逻辑/行业空间/竞争格局/催化事件) |
|
|
@@ -376,8 +312,10 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
376
312
|
- `insight foreign-report list`
|
|
377
313
|
- `insight announcement list`
|
|
378
314
|
- `insight announcement-hk list`
|
|
315
|
+
- `insight announcement-us list`
|
|
379
316
|
- `insight foreign-opinion list`
|
|
380
317
|
- `insight independent-opinion list`
|
|
318
|
+
- `insight official-account list`
|
|
381
319
|
- `ai security-clue`
|
|
382
320
|
- `vault drive-list`
|
|
383
321
|
- `vault record-list`
|
|
@@ -395,7 +333,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
395
333
|
|
|
396
334
|
## 智能文件命名
|
|
397
335
|
|
|
398
|
-
下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
336
|
+
下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`announcement-hk download`、`announcement-us download`、`official-account download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
399
337
|
|
|
400
338
|
1. **缓存优先** — 如果之前执行过对应的 `list` 命令,标题已缓存在 `~/.config/gangtise/title-cache.json`,直接使用,无额外 API 调用
|
|
401
339
|
2. **API 回查** — 缓存未命中时,自动查询最近 200 条记录匹配标题
|
|
@@ -446,6 +384,11 @@ gangtise insight roadshow list --institution C100000017
|
|
|
446
384
|
# 港股公告
|
|
447
385
|
gangtise insight announcement-hk list --security 01913.HK --rank-type 2 --size 20 --format json
|
|
448
386
|
gangtise insight announcement-hk download --announcement-id ANN2026040200012345
|
|
387
|
+
gangtise insight announcement-hk download --announcement-id ANN2026040200012345 --file-type 2 # Markdown
|
|
388
|
+
|
|
389
|
+
# 美股公告(--security 用美股代码;分类用 reference constant-list --category usShareAnnouncementCategory)
|
|
390
|
+
gangtise insight announcement-us list --security TSLA.O --rank-type 2 --size 20 --format json
|
|
391
|
+
gangtise insight announcement-us download --announcement-id 49629029 --file-type 2 # Markdown
|
|
449
392
|
|
|
450
393
|
# 外资机构观点
|
|
451
394
|
gangtise insight foreign-opinion list --keyword "自动驾驶" --region us --rank-type 2 --format json
|
|
@@ -455,6 +398,10 @@ gangtise insight foreign-opinion list --security APP.O --rating buy --format jso
|
|
|
455
398
|
gangtise insight independent-opinion list --keyword "肿瘤" --industry 100800118 --format json
|
|
456
399
|
gangtise insight independent-opinion download --independent-opinion-id 207051900018372 --file-type 2
|
|
457
400
|
|
|
401
|
+
# 产业公众号资讯
|
|
402
|
+
gangtise insight official-account list --keyword 泡泡玛特 --rank-type 2 --size 20 --format json
|
|
403
|
+
gangtise insight official-account download --article-id 7286248 --file-type 2
|
|
404
|
+
|
|
458
405
|
# 纪要下载(会议平台来源可选 HTML 格式)
|
|
459
406
|
gangtise insight summary download --summary-id 4906813 --file-type 2
|
|
460
407
|
```
|
|
@@ -468,10 +415,15 @@ gangtise reference securities-search --keyword "600519" --category stock
|
|
|
468
415
|
gangtise reference securities-search --keyword gzmt --top 5
|
|
469
416
|
gangtise reference securities-search --keyword "银行" --category stock --category index
|
|
470
417
|
|
|
418
|
+
# 首席分析师 ID 搜索(按姓名/机构/团队;拿 chiefId 供 insight opinion list --chief 使用)
|
|
419
|
+
gangtise reference chiefs-search --keyword 东吴证券 --top 3 --format json
|
|
420
|
+
gangtise reference chiefs-search --keyword 芦哲 --format json
|
|
421
|
+
|
|
471
422
|
# 常量查询:先看分类,再按分类导出全量常量值
|
|
472
423
|
gangtise reference constant-category --format json
|
|
473
424
|
gangtise reference constant-list --category citicIndustry --format json
|
|
474
425
|
gangtise reference constant-list --category aShareAnnouncementCategory --format json # 树形,含 children
|
|
426
|
+
gangtise reference constant-list --category usShareAnnouncementCategory --format json # 美股公告分类(103980xxx 段)
|
|
475
427
|
|
|
476
428
|
# 题材 ID 搜索(供 concept-info / concept-securities / theme-tracking 使用)
|
|
477
429
|
gangtise reference concept-search --keyword 机器人 --top 3 --format json
|
|
@@ -546,6 +498,11 @@ gangtise fundamental balance-sheet-hk --security-code 09992.HK --fiscal-year 202
|
|
|
546
498
|
gangtise fundamental cash-flow-hk --security-code 09992.HK --fiscal-year 2025 --period annual --field netOpCashFlows --field netInvCashFlows --field netFinCashFlows
|
|
547
499
|
# 最新一期完整港股利润表
|
|
548
500
|
gangtise fundamental income-statement-hk --security-code 09992.HK --format json
|
|
501
|
+
|
|
502
|
+
# 美股三大报表(--security-code 用美股代码;period 同港股但无 h2)
|
|
503
|
+
gangtise fundamental income-statement-us --security-code TSLA.O --period latest --format json
|
|
504
|
+
gangtise fundamental balance-sheet-us --security-code TSLA.O --fiscal-year 2025 --period annual --field totalAssets --field totalLiab --field totalEquity
|
|
505
|
+
gangtise fundamental cash-flow-us --security-code TSLA.O --fiscal-year 2024 --fiscal-year 2025 --period annual --field netOpCashFlows
|
|
549
506
|
```
|
|
550
507
|
|
|
551
508
|
### AI
|
|
@@ -556,6 +513,9 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
556
513
|
gangtise ai knowledge-batch --query 新能源汽车 --resource-type 10 --resource-type 11 --top 10
|
|
557
514
|
gangtise ai security-clue --start-time "2026-04-01 00:00:00" --end-time "2026-04-09 23:59:59" --query-mode byIndustry --gts-code 821035.SWI --source researchReport --source announcement
|
|
558
515
|
gangtise ai one-pager --security-code 600519.SH
|
|
516
|
+
# 个股看点(精炼投研总结,仅 A 股/港股):传具体代码,或 aShares/hkStocks 拉全市场
|
|
517
|
+
gangtise ai stock-summary --security 600519.SH --security 00700.HK --format json
|
|
518
|
+
gangtise ai stock-summary --security hkStocks --format json
|
|
559
519
|
gangtise ai investment-logic --security-code 600519.SH
|
|
560
520
|
gangtise ai peer-comparison --security-code 600519.SH
|
|
561
521
|
gangtise ai earnings-review --security-code 600519.SH --period 2025q3
|
|
@@ -613,6 +573,34 @@ gangtise vault stock-pool-stocks --pool-id 808477293
|
|
|
613
573
|
gangtise vault stock-pool-stocks
|
|
614
574
|
```
|
|
615
575
|
|
|
576
|
+
### Indicator(证券级数据指标 EDE)
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# Step 1:按名称搜索,拿 indicatorCode(绝不猜编码);--format json 看可传参数 parameterList 及 required
|
|
580
|
+
gangtise indicator search --keyword 收盘价 --format table # → qte_close
|
|
581
|
+
gangtise indicator search --keyword 平均ROE --limit 5 --format json # 看 parameterList
|
|
582
|
+
|
|
583
|
+
# 截面:多指标 × 多证券,单日快照(行情类用交易日;财务类用报告期末,如 2026-03-31)
|
|
584
|
+
gangtise indicator cross-section \
|
|
585
|
+
--indicator qte_close --indicator qte_vol --indicator qte_mkt_cptl \
|
|
586
|
+
--security 600519.SH --security 09992.HK \
|
|
587
|
+
--date 2026-05-18 --format table
|
|
588
|
+
|
|
589
|
+
# 时间序列:多指标 × 单证券 或 单指标 × 多证券(不能多 × 多,否则报 410001)
|
|
590
|
+
gangtise indicator time-series --indicator qte_close \
|
|
591
|
+
--security 600519.SH --security 09992.HK \
|
|
592
|
+
--start-date 2026-05-12 --end-date 2026-05-18 --format table
|
|
593
|
+
|
|
594
|
+
# 复权 / 指标专属参数用 --indicator-param "code:key=value",参数 key 以 search 的 parameterList 为准
|
|
595
|
+
gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
596
|
+
--date 2026-05-18 --indicator-param "qte_close:adjustmentType=3" # 1不复权/2前复权/3后复权
|
|
597
|
+
|
|
598
|
+
# 必填参数:很多指标默认调用报 410106(缺必填参数),按 parameterList 的 required 补齐再取:
|
|
599
|
+
# N 期统计补 periodNum、区间/周期类(如 qte_amp_mo 月振幅)补 startDate、年度/分红类补 fiscalYear
|
|
600
|
+
gangtise indicator cross-section --indicator finc_roe_avg_avg --security 600519.SH \
|
|
601
|
+
--date 2026-03-31 --indicator-param "finc_roe_avg_avg:periodNum=4"
|
|
602
|
+
```
|
|
603
|
+
|
|
616
604
|
### Alternative(行业指标数据库 EDB)
|
|
617
605
|
|
|
618
606
|
```bash
|
|
@@ -694,3 +682,22 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
|
|
|
694
682
|
| `430007` | 行情查询超出限制(数据量过大,请缩短日期范围或减少 `--limit`) |
|
|
695
683
|
| `410110` | 异步任务生成中(非终态,需继续轮询) |
|
|
696
684
|
| `410111` | 异步任务生成失败(终态,不可重试) |
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## 发布(维护者)
|
|
689
|
+
|
|
690
|
+
> 面向仓库维护者的发版流程,普通用户可跳过。
|
|
691
|
+
|
|
692
|
+
npm 发版通过 GitHub Actions Trusted Publishing 完成,不需要 `NPM_TOKEN`。npm 包设置里的 Trusted Publisher 需要匹配本仓库和 workflow 文件名 `publish.yml`。
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
npm version patch --no-git-tag-version
|
|
696
|
+
npm run prepare
|
|
697
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
698
|
+
git commit -am "chore: release v$VERSION"
|
|
699
|
+
git tag -a "v$VERSION" -m "v$VERSION" # 必须 annotated:--follow-tags 不推 lightweight tag
|
|
700
|
+
git push --follow-tags
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
推送 `v*` tag 后,`.github/workflows/publish.yml` 会在 GitHub-hosted runner 上使用 OIDC 发布到 `https://registry.npmjs.org/`。也可以从 GitHub Actions 页面手动运行该 workflow。
|
package/dist/src/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
3
|
import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/asyncContent.js";
|
|
4
|
-
import { readTokenCache } from "./core/auth.js";
|
|
4
|
+
import { readTokenCache, redactTokenCache } from "./core/auth.js";
|
|
5
5
|
import { collectKeyValue, collectList, collectNumberList, maybeArray, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
|
|
6
|
-
import { buildQuoteKlineBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
|
|
6
|
+
import { buildIndicatorCrossSectionBody, buildIndicatorTimeSeriesBody, buildQuoteKlineBody, buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
|
|
7
|
+
import { flattenCrossSection, flattenTimeSeries, unwrapIndicatorData } from "./core/indicatorMatrix.js";
|
|
7
8
|
import { callKlineWithSharding } from "./core/quoteSharding.js";
|
|
8
9
|
import { loadConfig } from "./core/config.js";
|
|
9
10
|
import { resolveTitle, saveDownloadResult } from "./core/download.js";
|
|
10
11
|
import { ENDPOINTS } from "./core/endpoints.js";
|
|
11
|
-
import { ApiError, ConfigError } from "./core/errors.js";
|
|
12
|
+
import { ApiError, ConfigError, ValidationError } from "./core/errors.js";
|
|
12
13
|
import { normalizeRows } from "./core/normalize.js";
|
|
13
14
|
import { parseOutputFormat } from "./core/output.js";
|
|
14
15
|
import { printData } from "./core/printer.js";
|
|
@@ -99,14 +100,18 @@ program
|
|
|
99
100
|
.command("auth")
|
|
100
101
|
.description("Authentication commands")
|
|
101
102
|
.addCommand(new Command("login")
|
|
103
|
+
.option("--show-token", "Show the raw access token (default: redacted)")
|
|
102
104
|
.option("--format <format>", "Output format", "json")
|
|
103
|
-
.action((options) => emit(options, (client) =>
|
|
105
|
+
.action((options) => emit(options, async (client) => {
|
|
106
|
+
const result = await client.login();
|
|
107
|
+
return options.showToken ? result : { authorization: "<redacted>", cache: redactTokenCache(result.cache) };
|
|
108
|
+
})))
|
|
104
109
|
.addCommand(new Command("status")
|
|
105
110
|
.option("--format <format>", "Output format", "json")
|
|
106
111
|
.action(async (options) => {
|
|
107
112
|
const config = loadConfig();
|
|
108
113
|
const cache = await readTokenCache(config.tokenCachePath);
|
|
109
|
-
await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseOutputFormat(options.format));
|
|
114
|
+
await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache: redactTokenCache(cache) }, parseOutputFormat(options.format));
|
|
110
115
|
}));
|
|
111
116
|
const lookup = new Command("lookup").description("Local lookup tables (IDs not covered by 'reference constant-list')");
|
|
112
117
|
const addLookupList = (name, endpointKey, description) => {
|
|
@@ -129,6 +134,7 @@ const research = new Command("research");
|
|
|
129
134
|
const foreignReport = new Command("foreign-report");
|
|
130
135
|
const announcement = new Command("announcement");
|
|
131
136
|
const announcementHk = new Command("announcement-hk");
|
|
137
|
+
const announcementUs = new Command("announcement-us");
|
|
132
138
|
const foreignOpinion = new Command("foreign-opinion");
|
|
133
139
|
const independentOpinion = new Command("independent-opinion");
|
|
134
140
|
const officialAccount = new Command("official-account");
|
|
@@ -214,6 +220,12 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
|
|
|
214
220
|
minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
|
|
215
221
|
}), { endpointKey: "insight.foreign-report.list", idField: "reportId" }));
|
|
216
222
|
addDownloadCommand(foreignReport, { endpointKey: "insight.foreign-report.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "foreign-report", fileType: { description: "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", default: "1" }, titleListEndpoint: "insight.foreign-report.list" });
|
|
223
|
+
// Contract: A-share announcement startTime/endTime go out as 13-digit epoch millis
|
|
224
|
+
// (parseTimestamp13), while HK/US announcement and every other insight list send the
|
|
225
|
+
// datetime string straight through. All three filter correctly — verified live against
|
|
226
|
+
// a narrow past window (each returns in-window rows). A-share's API also accepts the
|
|
227
|
+
// string form, but the 13-digit conversion is kept as the historical spec contract;
|
|
228
|
+
// don't "unify" it away without re-confirming the A-share announcement spec.
|
|
217
229
|
addTimeFilters(announcement.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement.list", {
|
|
218
230
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
219
231
|
startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
|
|
@@ -229,7 +241,16 @@ addTimeFilters(announcementHk.command("list").option("--search-type <number>", "
|
|
|
229
241
|
keyword: options.keyword,
|
|
230
242
|
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
231
243
|
}), { endpointKey: "insight.announcement-hk.list", idField: "announcementId" }));
|
|
232
|
-
addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", titleListEndpoint: "insight.announcement-hk.list" });
|
|
244
|
+
addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", fileType: { description: "File type: 1=original 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement-hk.list" });
|
|
245
|
+
addTimeFilters(announcementUs.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. TSLA.O)", collectList, []).option("--category <id>", "Category ID (constant-list usShareAnnouncementCategory)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement-us.list", {
|
|
246
|
+
from: parseFrom(options.from), size: parseSize(options.size),
|
|
247
|
+
startTime: options.startTime, endTime: options.endTime,
|
|
248
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
|
|
249
|
+
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
250
|
+
keyword: options.keyword,
|
|
251
|
+
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
252
|
+
}), { endpointKey: "insight.announcement-us.list", idField: "announcementId" }));
|
|
253
|
+
addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
|
|
233
254
|
addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-opinion.list", {
|
|
234
255
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
235
256
|
startTime: options.startTime, endTime: options.endTime,
|
|
@@ -268,6 +289,7 @@ insight.addCommand(research);
|
|
|
268
289
|
insight.addCommand(foreignReport);
|
|
269
290
|
insight.addCommand(announcement);
|
|
270
291
|
insight.addCommand(announcementHk);
|
|
292
|
+
insight.addCommand(announcementUs);
|
|
271
293
|
insight.addCommand(foreignOpinion);
|
|
272
294
|
insight.addCommand(independentOpinion);
|
|
273
295
|
insight.addCommand(officialAccount);
|
|
@@ -299,6 +321,9 @@ addFinancialReport("cash-flow-quarterly", "fundamental.cash-flow-quarterly", "Pe
|
|
|
299
321
|
addFinancialReport("income-statement-hk", "fundamental.income-statement-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
300
322
|
addFinancialReport("balance-sheet-hk", "fundamental.balance-sheet-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
301
323
|
addFinancialReport("cash-flow-hk", "fundamental.cash-flow-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
324
|
+
addFinancialReport("income-statement-us", "fundamental.income-statement-us", "Period: q1/h1/q3/nsd/annual/latest");
|
|
325
|
+
addFinancialReport("balance-sheet-us", "fundamental.balance-sheet-us", "Period: q1/h1/q3/nsd/annual/latest");
|
|
326
|
+
addFinancialReport("cash-flow-us", "fundamental.cash-flow-us", "Period: q1/h1/q3/nsd/annual/latest");
|
|
302
327
|
fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) })));
|
|
303
328
|
fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
304
329
|
let data = await client.call("fundamental.valuation-analysis", { securityCode: options.securityCode, indicator: options.indicator, startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) });
|
|
@@ -327,7 +352,11 @@ fundamental.command("earning-forecast").requiredOption("--security-code <code>")
|
|
|
327
352
|
}));
|
|
328
353
|
program.addCommand(fundamental);
|
|
329
354
|
const ai = new Command("ai").description("AI APIs");
|
|
330
|
-
ai.command("knowledge-batch").
|
|
355
|
+
ai.command("knowledge-batch").option("--query <text>", "Query", collectList, []).option("--top <number>", "Top", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <ms>").option("--end-time <ms>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => {
|
|
356
|
+
if (!options.query.length)
|
|
357
|
+
throw new ValidationError("--query is required: pass at least one --query");
|
|
358
|
+
return emit(options, (client) => client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseOptionalNumberOption(options.startTime, "--start-time", { integer: true, min: 0 }), endTime: parseOptionalNumberOption(options.endTime, "--end-time", { integer: true, min: 0 }) }));
|
|
359
|
+
});
|
|
331
360
|
ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
|
|
332
361
|
await runDownload(client, "ai.knowledge-resource.download", { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }, {
|
|
333
362
|
output: options.output,
|
|
@@ -371,8 +400,8 @@ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option
|
|
|
371
400
|
startDate: options.startDate,
|
|
372
401
|
endDate: options.endDate,
|
|
373
402
|
categoryList: options.category.length > 0 ? options.category : ALL_CATEGORIES,
|
|
374
|
-
withRelatedSecurities: options.withRelatedSecurities
|
|
375
|
-
withCloseReading: options.withCloseReading
|
|
403
|
+
withRelatedSecurities: options.withRelatedSecurities !== false,
|
|
404
|
+
withCloseReading: options.withCloseReading !== false,
|
|
376
405
|
});
|
|
377
406
|
}));
|
|
378
407
|
ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-announcement", {
|
|
@@ -405,6 +434,13 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
|
|
|
405
434
|
}
|
|
406
435
|
}));
|
|
407
436
|
ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId from viewpoint-debate").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
|
|
437
|
+
ai.command("stock-summary").description("Stock highlights: refined research summary per security (A-share / HK)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK), or market keyword: aShares / hkStocks; max 6000", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => {
|
|
438
|
+
// Guard against an empty --security: omitting it would send securityList:undefined,
|
|
439
|
+
// which the backend may treat as all-market (3 credits/row × thousands of rows).
|
|
440
|
+
if (!options.security.length)
|
|
441
|
+
throw new ValidationError("--security is required: pass security code(s) or a market keyword (aShares / hkStocks)");
|
|
442
|
+
return emit(options, (client) => client.call("ai.stock-summary.list", { securityList: maybeArray(options.security) }));
|
|
443
|
+
});
|
|
408
444
|
const reference = new Command("reference").description("Reference data APIs");
|
|
409
445
|
reference.command("securities-search").requiredOption("--keyword <text>", "Search keyword (name/code/pinyin/English)").option("--category <type>", "Category: stock/dr/index/fund", collectList, []).option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.securities-search", {
|
|
410
446
|
keyword: options.keyword,
|
|
@@ -422,6 +458,10 @@ reference.command("sector-search").option("--keyword <text>", "Search keyword (n
|
|
|
422
458
|
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
423
459
|
})));
|
|
424
460
|
reference.command("sector-constituents").requiredOption("--sector-id <id>", "Sector ID from 'reference sector-search'").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.sector-constituents", { sectorId: options.sectorId })));
|
|
461
|
+
reference.command("chiefs-search").requiredOption("--keyword <text>", "Search keyword (chief name / institution / team)").option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.chiefs-search", {
|
|
462
|
+
keyword: options.keyword,
|
|
463
|
+
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
464
|
+
})));
|
|
425
465
|
program.addCommand(reference);
|
|
426
466
|
const vault = new Command("vault").description("Vault APIs");
|
|
427
467
|
vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.drive.list", idField: "fileId" }));
|
|
@@ -433,7 +473,7 @@ addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "
|
|
|
433
473
|
vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
|
|
434
474
|
vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Rows to return", "20").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))));
|
|
435
475
|
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", {})));
|
|
436
|
-
vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple;
|
|
476
|
+
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))));
|
|
437
477
|
program.addCommand(vault);
|
|
438
478
|
program.addCommand(ai);
|
|
439
479
|
const alternative = new Command("alternative").description("Alternative data APIs");
|
|
@@ -460,6 +500,23 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
|
|
|
460
500
|
alternative.command("concept-info").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-info", { conceptId: options.conceptId })));
|
|
461
501
|
alternative.command("concept-securities").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-securities", { conceptId: options.conceptId })));
|
|
462
502
|
program.addCommand(alternative);
|
|
503
|
+
const indicator = new Command("indicator").description("Data indicator (EDE) APIs: search codes, cross-section, time-series");
|
|
504
|
+
indicator.command("search").requiredOption("--keyword <text>", "Search keyword, e.g. '收盘价' '成交量' '营业收入' (not free-form questions)").option("--limit <number>", "Max results (default: 50, max: 100)", "50").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
505
|
+
const raw = await client.call("indicator.search", {
|
|
506
|
+
keyword: options.keyword,
|
|
507
|
+
limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
|
|
508
|
+
});
|
|
509
|
+
await printData(unwrapIndicatorData(raw), parseOutputFormat(options.format), options.output);
|
|
510
|
+
}));
|
|
511
|
+
indicator.command("cross-section").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--date <date>", "Data date (yyyy-MM-dd)").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:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
512
|
+
const raw = await client.call("indicator.cross-section", buildIndicatorCrossSectionBody(options));
|
|
513
|
+
await printData(flattenCrossSection(unwrapIndicatorData(raw)), parseOutputFormat(options.format), options.output);
|
|
514
|
+
}));
|
|
515
|
+
indicator.command("time-series").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday (default TD)").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:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
516
|
+
const raw = await client.call("indicator.time-series", buildIndicatorTimeSeriesBody(options));
|
|
517
|
+
await printData(flattenTimeSeries(unwrapIndicatorData(raw)), parseOutputFormat(options.format), options.output);
|
|
518
|
+
}));
|
|
519
|
+
program.addCommand(indicator);
|
|
463
520
|
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) => {
|
|
464
521
|
const endpoint = ENDPOINTS[endpointKey];
|
|
465
522
|
if (!endpoint) {
|
package/dist/src/core/args.js
CHANGED
|
@@ -79,3 +79,29 @@ export function parseTimestamp13(value, optionName) {
|
|
|
79
79
|
}
|
|
80
80
|
return parsed;
|
|
81
81
|
}
|
|
82
|
+
// Parse repeatable `--indicator-param "code:key=value"` specs into the nested
|
|
83
|
+
// indicatorParamList the EDE cross-section / time-series endpoints expect.
|
|
84
|
+
// Multiple specs for the same code accumulate into one group, first-seen order.
|
|
85
|
+
export function parseIndicatorParams(specs) {
|
|
86
|
+
if (specs.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const groups = new Map();
|
|
89
|
+
for (const spec of specs) {
|
|
90
|
+
const colon = spec.indexOf(":");
|
|
91
|
+
const rest = colon === -1 ? "" : spec.slice(colon + 1);
|
|
92
|
+
const eq = rest.indexOf("=");
|
|
93
|
+
const code = colon === -1 ? "" : spec.slice(0, colon).trim();
|
|
94
|
+
const paramKey = eq === -1 ? "" : rest.slice(0, eq).trim();
|
|
95
|
+
const paramValue = eq === -1 ? "" : rest.slice(eq + 1).trim();
|
|
96
|
+
if (!code || !paramKey) {
|
|
97
|
+
throw new ValidationError(`Invalid --indicator-param: expected "code:key=value", got "${spec}"`);
|
|
98
|
+
}
|
|
99
|
+
let group = groups.get(code);
|
|
100
|
+
if (!group) {
|
|
101
|
+
group = { indicatorCode: code, parameters: [] };
|
|
102
|
+
groups.set(code, group);
|
|
103
|
+
}
|
|
104
|
+
group.parameters.push({ paramKey, paramValue });
|
|
105
|
+
}
|
|
106
|
+
return [...groups.values()];
|
|
107
|
+
}
|
package/dist/src/core/auth.js
CHANGED
|
@@ -14,6 +14,23 @@ export async function readTokenCache(filePath) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Return a display-safe copy of a token cache for `auth status`: any field whose
|
|
19
|
+
* name matches a credential pattern (token / key / secret / password / credential)
|
|
20
|
+
* is replaced with "<redacted>" so the raw bearer token — or any unknown credential
|
|
21
|
+
* field the cache file might carry (apiKey, privateKey, …) — is never printed; all
|
|
22
|
+
* other metadata (expiresAt, userName, productCode, …) is preserved.
|
|
23
|
+
*/
|
|
24
|
+
export function redactTokenCache(cache) {
|
|
25
|
+
if (!cache)
|
|
26
|
+
return null;
|
|
27
|
+
const SENSITIVE = /token|secret|password|credential|key/i;
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [key, value] of Object.entries(cache)) {
|
|
30
|
+
out[key] = SENSITIVE.test(key) ? "<redacted>" : value;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
17
34
|
export async function writeTokenCache(filePath, cache) {
|
|
18
35
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
19
36
|
await fs.writeFile(filePath, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
@@ -30,7 +47,12 @@ export function normalizeToken(token) {
|
|
|
30
47
|
}
|
|
31
48
|
export function requireAccessCredentials(accessKey, secretKey) {
|
|
32
49
|
if (!accessKey || !secretKey) {
|
|
33
|
-
|
|
50
|
+
const missing = [!accessKey && "GANGTISE_ACCESS_KEY", !secretKey && "GANGTISE_SECRET_KEY"].filter(Boolean).join(", ");
|
|
51
|
+
throw new ConfigError(`缺少环境变量: ${missing}(未导出到当前进程环境)\n`
|
|
52
|
+
+ `注意:在 shell 里赋值还不够,必须"导出",子进程才读得到:\n`
|
|
53
|
+
+ ` bash/zsh: export GANGTISE_ACCESS_KEY=... GANGTISE_SECRET_KEY=...\n`
|
|
54
|
+
+ ` fish: set -gx GANGTISE_ACCESS_KEY ...; set -gx GANGTISE_SECRET_KEY ...\n`
|
|
55
|
+
+ `验证:env | grep GANGTISE(能列出对应行才算导出成功)`);
|
|
34
56
|
}
|
|
35
57
|
return { accessKey, secretKey };
|
|
36
58
|
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -17,11 +17,14 @@ export class GangtiseClient {
|
|
|
17
17
|
config;
|
|
18
18
|
refreshPromise = null;
|
|
19
19
|
memoCache = null;
|
|
20
|
+
// After an injected env token (GANGTISE_TOKEN) is rejected and we self-heal via
|
|
21
|
+
// login, stop preferring that now-stale token so the retry uses the fresh one.
|
|
22
|
+
envTokenInvalidated = false;
|
|
20
23
|
constructor(config) {
|
|
21
24
|
this.config = config;
|
|
22
25
|
}
|
|
23
26
|
async getAuthorizationHeader(forceRefresh = false) {
|
|
24
|
-
if (this.config.token && !forceRefresh) {
|
|
27
|
+
if (this.config.token && !this.envTokenInvalidated && !forceRefresh) {
|
|
25
28
|
return normalizeToken(this.config.token);
|
|
26
29
|
}
|
|
27
30
|
if (!forceRefresh) {
|
|
@@ -69,6 +72,7 @@ export class GangtiseClient {
|
|
|
69
72
|
&& this.config.secretKey) {
|
|
70
73
|
authState.retried = true;
|
|
71
74
|
this.memoCache = null;
|
|
75
|
+
this.envTokenInvalidated = true;
|
|
72
76
|
await this.getAuthorizationHeader(true);
|
|
73
77
|
throw markRetryable(new ApiError(error.message, error.code, error.statusCode, error.details));
|
|
74
78
|
}
|
|
@@ -175,19 +179,40 @@ export class GangtiseClient {
|
|
|
175
179
|
}
|
|
176
180
|
let unexpectedShape = false;
|
|
177
181
|
let totalDrift = false;
|
|
182
|
+
// Fail-soft fan-out: a hard page failure (rate-limit 903301, no-perm, retries
|
|
183
|
+
// exhausted) must NOT discard the pages already fetched. Catch per page, record
|
|
184
|
+
// it, and stop starting new requests so we don't keep burning quota into a rate
|
|
185
|
+
// limit. Mirrors quoteSharding's partial-result tolerance — but firstPage already
|
|
186
|
+
// succeeded to get here, so unlike sharding there's no total-failure case.
|
|
187
|
+
const failedPages = [];
|
|
188
|
+
let firstError = null;
|
|
189
|
+
let aborted = false;
|
|
178
190
|
const pages = await runWithConcurrency(pageRequests, PAGINATION_CONCURRENCY, async (req) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
if (aborted) {
|
|
192
|
+
failedPages.push(req);
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const page = await this.requestJson(endpoint, {
|
|
197
|
+
...initialBody,
|
|
198
|
+
from: req.from,
|
|
199
|
+
size: req.size,
|
|
200
|
+
});
|
|
201
|
+
if (!this.isPaginatedListResponse(page)) {
|
|
202
|
+
unexpectedShape = true;
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
if (page.total !== total)
|
|
206
|
+
totalDrift = true;
|
|
207
|
+
return page.list;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!firstError)
|
|
211
|
+
firstError = error;
|
|
212
|
+
aborted = true;
|
|
213
|
+
failedPages.push(req);
|
|
186
214
|
return [];
|
|
187
215
|
}
|
|
188
|
-
if (page.total !== total)
|
|
189
|
-
totalDrift = true;
|
|
190
|
-
return page.list;
|
|
191
216
|
});
|
|
192
217
|
for (const list of pages) {
|
|
193
218
|
if (list.length === 0)
|
|
@@ -206,11 +231,18 @@ export class GangtiseClient {
|
|
|
206
231
|
if (truncatedByPageCap) {
|
|
207
232
|
process.stderr.write(`[gangtise] warning: hit the ${MAX_PAGES}-page safety cap; fetched ${collected.length} of ${total} rows. Narrow the query (e.g. a shorter date range) or pass --size to fetch a bounded subset.\n`);
|
|
208
233
|
}
|
|
209
|
-
|
|
234
|
+
const out = {
|
|
210
235
|
...firstPage,
|
|
211
236
|
total,
|
|
212
237
|
list: requestedSize === undefined ? collected : collected.slice(0, requestedSize),
|
|
213
238
|
};
|
|
239
|
+
if (failedPages.length > 0) {
|
|
240
|
+
out.partial = true;
|
|
241
|
+
out.failedPages = failedPages.map((p) => ({ from: p.from, size: p.size }));
|
|
242
|
+
const detail = firstError instanceof Error ? `: ${firstError.message}` : "";
|
|
243
|
+
process.stderr.write(`[gangtise] warning: ${failedPages.length}/${pageRequests.length} pages not fetched${detail}; results are partial — got ${collected.length}/${total} rows (see failedPages). A page hit a non-retryable error (e.g. rate limit); remaining pages were skipped.\n`);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
214
246
|
}
|
|
215
247
|
async login() {
|
|
216
248
|
const authorization = await this.getAuthorizationHeader();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { maybeArray, parseFrom, parseOptionalNumberOption, parseSize } from "./args.js";
|
|
1
|
+
import { maybeArray, parseFrom, parseIndicatorParams, parseOptionalNumberOption, parseSize } from "./args.js";
|
|
2
2
|
export function buildQuoteKlineBody(options) {
|
|
3
3
|
return {
|
|
4
4
|
securityList: maybeArray(options.security),
|
|
@@ -29,3 +29,30 @@ export function buildWechatChatroomListBody(options) {
|
|
|
29
29
|
roomName: options.roomName.length > 0 ? options.roomName.join(",") : undefined,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
export function buildStockPoolStocksBody(options) {
|
|
33
|
+
return {
|
|
34
|
+
poolIdList: options.poolId?.length ? options.poolId : ["all"],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function buildIndicatorCrossSectionBody(options) {
|
|
38
|
+
return {
|
|
39
|
+
indicatorCodeList: maybeArray(options.indicator),
|
|
40
|
+
securityCodeList: maybeArray(options.security),
|
|
41
|
+
date: options.date,
|
|
42
|
+
currency: options.currency,
|
|
43
|
+
scale: options.scale,
|
|
44
|
+
indicatorParamList: parseIndicatorParams(options.indicatorParam),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function buildIndicatorTimeSeriesBody(options) {
|
|
48
|
+
return {
|
|
49
|
+
indicatorCodeList: maybeArray(options.indicator),
|
|
50
|
+
securityCodeList: maybeArray(options.security),
|
|
51
|
+
startDate: options.startDate,
|
|
52
|
+
endDate: options.endDate,
|
|
53
|
+
calendarType: options.calendarType,
|
|
54
|
+
currency: options.currency,
|
|
55
|
+
scale: options.scale,
|
|
56
|
+
indicatorParamList: parseIndicatorParams(options.indicatorParam),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -2,6 +2,12 @@ import { extname } from "node:path";
|
|
|
2
2
|
import { DownloadError } from "./errors.js";
|
|
3
3
|
import { saveOutputIfNeeded } from "./output.js";
|
|
4
4
|
import { lookupTitleCache, readTitleCache, TITLE_LOOKUP_SIZE } from "./titleCache.js";
|
|
5
|
+
/** Replace filesystem-unsafe characters with `_` so a title or a server-supplied
|
|
6
|
+
* filename can't create stray subdirectories or escape the intended output path.
|
|
7
|
+
* Shared by title-based naming and the download fallback. */
|
|
8
|
+
function sanitizeFilename(name) {
|
|
9
|
+
return name.replace(/[/\\:*?"<>|]/g, "_");
|
|
10
|
+
}
|
|
5
11
|
const MIME_EXT = {
|
|
6
12
|
"application/pdf": ".pdf",
|
|
7
13
|
"application/msword": ".doc",
|
|
@@ -38,7 +44,7 @@ export async function resolveTitle(client, result, listEndpoint, idField, idValu
|
|
|
38
44
|
const file = result;
|
|
39
45
|
const serverExt = file.filename ? extname(file.filename) : extFromContentType(file.contentType);
|
|
40
46
|
function buildFilename(rawTitle) {
|
|
41
|
-
let title = rawTitle
|
|
47
|
+
let title = sanitizeFilename(rawTitle).trim();
|
|
42
48
|
if (serverExt && !title.toLowerCase().endsWith(serverExt.toLowerCase())) {
|
|
43
49
|
title += serverExt;
|
|
44
50
|
}
|
|
@@ -76,7 +82,9 @@ export async function saveDownloadResult(result, fallbackName, output) {
|
|
|
76
82
|
return;
|
|
77
83
|
}
|
|
78
84
|
if (file.data instanceof Uint8Array) {
|
|
79
|
-
|
|
85
|
+
// Sanitize the server-provided filename so a Content-Disposition value with
|
|
86
|
+
// / or : can't write outside the intended path (same rule as buildFilename).
|
|
87
|
+
const outputPath = output ?? (file.filename ? sanitizeFilename(file.filename) : undefined) ?? (fallbackName + extFromContentType(file.contentType));
|
|
80
88
|
await saveOutputIfNeeded(file.data, outputPath);
|
|
81
89
|
process.stdout.write(`${outputPath}\n`);
|
|
82
90
|
return;
|
|
@@ -138,6 +138,21 @@ export const ENDPOINTS = {
|
|
|
138
138
|
kind: "download",
|
|
139
139
|
description: "Download HK announcement file",
|
|
140
140
|
},
|
|
141
|
+
"insight.announcement-us.list": {
|
|
142
|
+
key: "insight.announcement-us.list",
|
|
143
|
+
method: "POST",
|
|
144
|
+
path: "/application/open-insight/announcement-us/getList",
|
|
145
|
+
kind: "json",
|
|
146
|
+
description: "List US announcements",
|
|
147
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
148
|
+
},
|
|
149
|
+
"insight.announcement-us.download": {
|
|
150
|
+
key: "insight.announcement-us.download",
|
|
151
|
+
method: "GET",
|
|
152
|
+
path: "/application/open-insight/announcement-us/download/file",
|
|
153
|
+
kind: "download",
|
|
154
|
+
description: "Download US announcement file",
|
|
155
|
+
},
|
|
141
156
|
"insight.foreign-opinion.list": {
|
|
142
157
|
key: "insight.foreign-opinion.list",
|
|
143
158
|
method: "POST",
|
|
@@ -184,6 +199,13 @@ export const ENDPOINTS = {
|
|
|
184
199
|
kind: "json",
|
|
185
200
|
description: "Search GTS codes (securities)",
|
|
186
201
|
},
|
|
202
|
+
"reference.chiefs-search": {
|
|
203
|
+
key: "reference.chiefs-search",
|
|
204
|
+
method: "POST",
|
|
205
|
+
path: "/application/open-reference/chiefs/search",
|
|
206
|
+
kind: "json",
|
|
207
|
+
description: "Search chief analyst IDs by name / institution / team",
|
|
208
|
+
},
|
|
187
209
|
"reference.constant-category": {
|
|
188
210
|
key: "reference.constant-category",
|
|
189
211
|
method: "GET",
|
|
@@ -319,6 +341,27 @@ export const ENDPOINTS = {
|
|
|
319
341
|
kind: "json",
|
|
320
342
|
description: "Query HK cash flow statement (China GAAP)",
|
|
321
343
|
},
|
|
344
|
+
"fundamental.income-statement-us": {
|
|
345
|
+
key: "fundamental.income-statement-us",
|
|
346
|
+
method: "POST",
|
|
347
|
+
path: "/application/open-fundamental/financial-report/income-statement/us",
|
|
348
|
+
kind: "json",
|
|
349
|
+
description: "Query US income statement",
|
|
350
|
+
},
|
|
351
|
+
"fundamental.balance-sheet-us": {
|
|
352
|
+
key: "fundamental.balance-sheet-us",
|
|
353
|
+
method: "POST",
|
|
354
|
+
path: "/application/open-fundamental/financial-report/balance-sheet/us",
|
|
355
|
+
kind: "json",
|
|
356
|
+
description: "Query US balance sheet",
|
|
357
|
+
},
|
|
358
|
+
"fundamental.cash-flow-us": {
|
|
359
|
+
key: "fundamental.cash-flow-us",
|
|
360
|
+
method: "POST",
|
|
361
|
+
path: "/application/open-fundamental/financial-report/cash-flow-statement/us",
|
|
362
|
+
kind: "json",
|
|
363
|
+
description: "Query US cash flow statement",
|
|
364
|
+
},
|
|
322
365
|
"fundamental.main-business": {
|
|
323
366
|
key: "fundamental.main-business",
|
|
324
367
|
method: "POST",
|
|
@@ -348,6 +391,13 @@ export const ENDPOINTS = {
|
|
|
348
391
|
description: "Query earning forecast (consensus estimates)",
|
|
349
392
|
},
|
|
350
393
|
// ─── ai ───
|
|
394
|
+
"ai.stock-summary.list": {
|
|
395
|
+
key: "ai.stock-summary.list",
|
|
396
|
+
method: "POST",
|
|
397
|
+
path: "/application/open-ai/stock-summary/getList",
|
|
398
|
+
kind: "json",
|
|
399
|
+
description: "Stock highlights (refined research summary per security)",
|
|
400
|
+
},
|
|
351
401
|
"ai.knowledge-batch": {
|
|
352
402
|
key: "ai.knowledge-batch",
|
|
353
403
|
method: "POST",
|
|
@@ -559,4 +609,26 @@ export const ENDPOINTS = {
|
|
|
559
609
|
kind: "json",
|
|
560
610
|
description: "Query concept (theme index) constituent securities, grouped",
|
|
561
611
|
},
|
|
612
|
+
// ─── indicator (EDE: security-level data indicators) ───
|
|
613
|
+
"indicator.search": {
|
|
614
|
+
key: "indicator.search",
|
|
615
|
+
method: "POST",
|
|
616
|
+
path: "/application/open-indicator/EDE/search",
|
|
617
|
+
kind: "json",
|
|
618
|
+
description: "Search data indicators by keyword (returns indicatorCode + params)",
|
|
619
|
+
},
|
|
620
|
+
"indicator.cross-section": {
|
|
621
|
+
key: "indicator.cross-section",
|
|
622
|
+
method: "POST",
|
|
623
|
+
path: "/application/open-indicator/EDE/cross-section",
|
|
624
|
+
kind: "json",
|
|
625
|
+
description: "Get cross-section data (multi-indicator x multi-security, single date)",
|
|
626
|
+
},
|
|
627
|
+
"indicator.time-series": {
|
|
628
|
+
key: "indicator.time-series",
|
|
629
|
+
method: "POST",
|
|
630
|
+
path: "/application/open-indicator/EDE/time-series",
|
|
631
|
+
kind: "json",
|
|
632
|
+
description: "Get time-series data (multi-indicator x single-security OR single-indicator x multi-security)",
|
|
633
|
+
},
|
|
562
634
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ApiError } from "./errors.js";
|
|
2
|
+
// The EDE endpoints double-wrap on success: the shared client strips the outer
|
|
3
|
+
// envelope but leaves an inner { code, status, data } around the real payload.
|
|
4
|
+
// Peel that inner envelope so the list (search) / matrix (cross-section,
|
|
5
|
+
// time-series) is reachable. Observed errors arrive single-enveloped (the
|
|
6
|
+
// client throws on those), but a failure code carried only by the inner
|
|
7
|
+
// envelope must still surface instead of rendering its null payload as success.
|
|
8
|
+
export function unwrapIndicatorData(raw) {
|
|
9
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
10
|
+
const record = raw;
|
|
11
|
+
if ("data" in record && ("code" in record || "status" in record)) {
|
|
12
|
+
const code = record.code === undefined ? undefined : String(record.code);
|
|
13
|
+
const ok = record.status === true || code === "000000" || code === "0";
|
|
14
|
+
if (!ok) {
|
|
15
|
+
throw new ApiError(typeof record.msg === "string" && record.msg ? record.msg : "Indicator API request failed", code);
|
|
16
|
+
}
|
|
17
|
+
return record.data;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
function asStringArray(value) {
|
|
23
|
+
return Array.isArray(value) ? value.map((item) => String(item)) : undefined;
|
|
24
|
+
}
|
|
25
|
+
function rowOf(values, index) {
|
|
26
|
+
const row = values[index];
|
|
27
|
+
return Array.isArray(row) ? row : undefined;
|
|
28
|
+
}
|
|
29
|
+
// Build one column header per series. Prefer the human-readable name; on a
|
|
30
|
+
// duplicate name append the code so a column is never silently overwritten.
|
|
31
|
+
function buildHeaders(names, codes, count) {
|
|
32
|
+
const used = new Set();
|
|
33
|
+
const headers = [];
|
|
34
|
+
for (let i = 0; i < count; i++) {
|
|
35
|
+
const base = String(names?.[i] ?? codes?.[i] ?? `col${i}`);
|
|
36
|
+
let header = base;
|
|
37
|
+
let attempt = 1;
|
|
38
|
+
while (used.has(header)) {
|
|
39
|
+
const suffix = codes?.[i] ?? i;
|
|
40
|
+
header = attempt === 1 ? `${base} (${suffix})` : `${base} (${suffix})_${attempt}`;
|
|
41
|
+
attempt++;
|
|
42
|
+
}
|
|
43
|
+
used.add(header);
|
|
44
|
+
headers.push(header);
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
// Cross-section: one row per security, one column per indicator. The live
|
|
49
|
+
// `values` is a 2D [numIndicators][numSecurities] matrix in indicator-major
|
|
50
|
+
// order, so indicator i on security j is values[i][j].
|
|
51
|
+
export function flattenCrossSection(data) {
|
|
52
|
+
if (!data || typeof data !== "object")
|
|
53
|
+
return data;
|
|
54
|
+
const d = data;
|
|
55
|
+
const securityCode = asStringArray(d.securityCodeList);
|
|
56
|
+
const indicators = asStringArray(d.indicatorCodeList);
|
|
57
|
+
if (!Array.isArray(d.values) || !securityCode || !indicators)
|
|
58
|
+
return data;
|
|
59
|
+
const securityName = asStringArray(d.securityNameList);
|
|
60
|
+
const headers = buildHeaders(asStringArray(d.indicatorNameList), indicators, indicators.length);
|
|
61
|
+
const list = securityCode.map((code, j) => {
|
|
62
|
+
const row = { date: d.date, security: code, name: securityName?.[j] };
|
|
63
|
+
for (let i = 0; i < indicators.length; i++) {
|
|
64
|
+
row[headers[i]] = rowOf(d.values, i)?.[j];
|
|
65
|
+
}
|
|
66
|
+
return row;
|
|
67
|
+
});
|
|
68
|
+
return { list, total: list.length };
|
|
69
|
+
}
|
|
70
|
+
// Time-series: one row per date. Columns are the indicators (single-security
|
|
71
|
+
// case) or the securities (single-indicator case) — exactly one dimension
|
|
72
|
+
// varies, per the API contract. `values` is a 2D [series][date] matrix.
|
|
73
|
+
export function flattenTimeSeries(data) {
|
|
74
|
+
if (!data || typeof data !== "object")
|
|
75
|
+
return data;
|
|
76
|
+
const d = data;
|
|
77
|
+
const dates = asStringArray(d.dates);
|
|
78
|
+
const securityCode = asStringArray(d.securityCodeList);
|
|
79
|
+
const indicators = asStringArray(d.indicatorCodeList);
|
|
80
|
+
if (!Array.isArray(d.values) || !dates || !securityCode || !indicators)
|
|
81
|
+
return data;
|
|
82
|
+
const seriesAreIndicators = securityCode.length <= 1;
|
|
83
|
+
const headers = seriesAreIndicators
|
|
84
|
+
? buildHeaders(asStringArray(d.indicatorNameList), indicators, indicators.length)
|
|
85
|
+
: buildHeaders(asStringArray(d.securityNameList), securityCode, securityCode.length);
|
|
86
|
+
const list = dates.map((date, k) => {
|
|
87
|
+
const row = { date };
|
|
88
|
+
for (let i = 0; i < headers.length; i++) {
|
|
89
|
+
row[headers[i]] = rowOf(d.values, i)?.[k];
|
|
90
|
+
}
|
|
91
|
+
return row;
|
|
92
|
+
});
|
|
93
|
+
return { list, total: list.length };
|
|
94
|
+
}
|
package/dist/src/core/output.js
CHANGED
|
@@ -141,7 +141,10 @@ function pickListForStreaming(value) {
|
|
|
141
141
|
}
|
|
142
142
|
function csvEscape(value) {
|
|
143
143
|
let out = value;
|
|
144
|
-
|
|
144
|
+
// Formula-injection guard, but don't mangle legitimate numbers: a leading
|
|
145
|
+
// -/+ only needs escaping when the cell isn't a finite number (e.g. "-1+cmd"),
|
|
146
|
+
// so values like "-3.5" stay numeric for Excel/pandas.
|
|
147
|
+
if (/^[=@\t\r]/.test(out) || (/^[+\-]/.test(out) && !Number.isFinite(Number(out))))
|
|
145
148
|
out = "'" + out;
|
|
146
149
|
if (/[",\n]/.test(out))
|
|
147
150
|
return `"${out.replaceAll("\"", "\"\"")}"`;
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated — DO NOT EDIT
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.20.0";
|