gangtise-openapi-cli 0.17.2 → 0.19.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 CHANGED
@@ -4,6 +4,28 @@
4
4
 
5
5
  ## Changelog
6
6
 
7
+ ### v0.19.0 — 2026-06-24
8
+
9
+ **新增接口(Indicator · 证券级数据指标 EDE)**
10
+ - `indicator search` — 按名称搜索证券级数据指标,返回 `indicatorCode` 及可传参数 `parameterList`(含 `required` 必填标记与枚举);取数前必先 search 拿 code,绝不猜编码
11
+ - `indicator cross-section` — 指标截面数据(多指标 × 多证券,单日快照):`--indicator` / `--security`(均可重复)/ `--date` / `--currency` / `--scale` / `--indicator-param`
12
+ - `indicator time-series` — 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间):另有 `--start-date` / `--end-date` / `--calendar-type`(`ND`/`TD`/`WD`)
13
+ - 复权等指标专属参数用 `--indicator-param "code:key=value"`,参数 key 与取值以 search 的 `parameterList` 为准(行情复权键为 `adjustmentType`:`1` 不复权 / `2` 前复权 / `3` 后复权)
14
+ - 很多指标有必填参数,默认调用会报 `410106`(缺必填参数):N 期统计补 `periodNum`、区间/周期类补 `startDate`、年度/分红类补 `fiscalYear`;`999999` 多为「该证券公司类型/报告期无数据」而非系统故障。详见 `gangtise-openapi/references/commands/indicator.md`
15
+
16
+ **修复**
17
+ - `vault stock-pool-stocks --pool-id <id>` 过滤失效:此前因选项默认值 `["all"]` 泄漏,传具体 pool id 仍返回全部股票池证券;现已修复——传 id 精确过滤,省略则默认全量
18
+ - `auth` 缺凭证报错补充跨 shell(bash/zsh/fish)的 `export` 提示
19
+
20
+ **文档**
21
+ - README / SKILL 补充 indicator 命令组与取数最佳实践;`official-account` 命令文档补全
22
+
23
+ ### v0.18.0 — 2026-06-17
24
+
25
+ **新增接口(Insight · 产业公众号资讯)**
26
+ - `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` 及关联行业/题材/证券列表
27
+ - `insight official-account download --article-id <id>` — 下载公众号文章:`--file-type 1` txt(默认)/ `2` HTML
28
+
7
29
  ### v0.17.0 — 2026-06-15
8
30
 
9
31
  **接口变更(Breaking)**
@@ -217,6 +239,7 @@ gangtise-openapi/
217
239
  │ ├── ai.md # AI 能力命令(one-pager / earnings-review / viewpoint-debate 等)
218
240
  │ ├── alternative.md # 行业指标数据库(EDB search / EDB data)
219
241
  │ ├── fundamental.md # 财务数据命令(A股/港股三大报表 / 估值 / 盈利预测 / 股东)
242
+ │ ├── indicator.md # 证券级数据指标 EDE(search / 截面 / 时序)
220
243
  │ ├── insight.md # 投研内容命令(研报 / 观点 / 纪要 / 公告 / 外资)
221
244
  │ ├── quote.md # 行情命令(A股/港股/指数 K 线)
222
245
  │ ├── reference-and-lookup.md # GTS Code 搜索与枚举速查
@@ -272,6 +295,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
272
295
  | | `announcement-hk list` / `download` | 港股公告(含下载) |
273
296
  | | `foreign-opinion list` | 外资机构观点 |
274
297
  | | `independent-opinion list` / `download` | 外资独立分析师观点(含原文/翻译HTML下载) |
298
+ | | `official-account list` / `download` | 产业公众号资讯(含 txt/HTML 下载) |
275
299
  | **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
276
300
  | | `constant-category` | 常量分类列表(含各分类适用的接口与参数) |
277
301
  | | `constant-list` | 按分类导出常量值全量列表(行业/城市/公告分类/区域等) |
@@ -307,6 +331,9 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
307
331
  | | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
308
332
  | | `wechat-message-list` / `wechat-chatroom-list` | 群消息列表与群ID查询 |
309
333
  | | `stock-pool-list` / `stock-pool-stocks` | 自选股股票池列表与证券明细 |
334
+ | **Indicator** | `search` | 证券级数据指标搜索(按名称匹配,返回 indicatorCode 及可传参数 parameterList) |
335
+ | | `cross-section` | 指标截面数据(多指标 × 多证券,单日快照;前置 `search` 拿 code) |
336
+ | | `time-series` | 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间) |
310
337
  | **Alternative** | `edb-search` | 行业指标搜索(按关键词匹配,返回 indicatorId 等元信息) |
311
338
  | | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标) |
312
339
  | | `concept-info` | 题材指数基本信息(投资逻辑/行业空间/竞争格局/催化事件) |
@@ -378,6 +405,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
378
405
  - `insight announcement-hk list`
379
406
  - `insight foreign-opinion list`
380
407
  - `insight independent-opinion list`
408
+ - `insight official-account list`
381
409
  - `ai security-clue`
382
410
  - `vault drive-list`
383
411
  - `vault record-list`
@@ -395,7 +423,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
395
423
 
396
424
  ## 智能文件命名
397
425
 
398
- 下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
426
+ 下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`official-account download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
399
427
 
400
428
  1. **缓存优先** — 如果之前执行过对应的 `list` 命令,标题已缓存在 `~/.config/gangtise/title-cache.json`,直接使用,无额外 API 调用
401
429
  2. **API 回查** — 缓存未命中时,自动查询最近 200 条记录匹配标题
@@ -455,6 +483,10 @@ gangtise insight foreign-opinion list --security APP.O --rating buy --format jso
455
483
  gangtise insight independent-opinion list --keyword "肿瘤" --industry 100800118 --format json
456
484
  gangtise insight independent-opinion download --independent-opinion-id 207051900018372 --file-type 2
457
485
 
486
+ # 产业公众号资讯
487
+ gangtise insight official-account list --keyword 泡泡玛特 --rank-type 2 --size 20 --format json
488
+ gangtise insight official-account download --article-id 7286248 --file-type 2
489
+
458
490
  # 纪要下载(会议平台来源可选 HTML 格式)
459
491
  gangtise insight summary download --summary-id 4906813 --file-type 2
460
492
  ```
@@ -613,6 +645,34 @@ gangtise vault stock-pool-stocks --pool-id 808477293
613
645
  gangtise vault stock-pool-stocks
614
646
  ```
615
647
 
648
+ ### Indicator(证券级数据指标 EDE)
649
+
650
+ ```bash
651
+ # Step 1:按名称搜索,拿 indicatorCode(绝不猜编码);--format json 看可传参数 parameterList 及 required
652
+ gangtise indicator search --keyword 收盘价 --format table # → qte_close
653
+ gangtise indicator search --keyword 平均ROE --limit 5 --format json # 看 parameterList
654
+
655
+ # 截面:多指标 × 多证券,单日快照(行情类用交易日;财务类用报告期末,如 2026-03-31)
656
+ gangtise indicator cross-section \
657
+ --indicator qte_close --indicator qte_vol --indicator qte_mkt_cptl \
658
+ --security 600519.SH --security 09992.HK \
659
+ --date 2026-05-18 --format table
660
+
661
+ # 时间序列:多指标 × 单证券 或 单指标 × 多证券(不能多 × 多,否则报 410001)
662
+ gangtise indicator time-series --indicator qte_close \
663
+ --security 600519.SH --security 09992.HK \
664
+ --start-date 2026-05-12 --end-date 2026-05-18 --format table
665
+
666
+ # 复权 / 指标专属参数用 --indicator-param "code:key=value",参数 key 以 search 的 parameterList 为准
667
+ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
668
+ --date 2026-05-18 --indicator-param "qte_close:adjustmentType=3" # 1不复权/2前复权/3后复权
669
+
670
+ # 必填参数:很多指标默认调用报 410106(缺必填参数),按 parameterList 的 required 补齐再取:
671
+ # N 期统计补 periodNum、区间/周期类(如 qte_amp_mo 月振幅)补 startDate、年度/分红类补 fiscalYear
672
+ gangtise indicator cross-section --indicator finc_roe_avg_avg --security 600519.SH \
673
+ --date 2026-03-31 --indicator-param "finc_roe_avg_avg:periodNum=4"
674
+ ```
675
+
616
676
  ### Alternative(行业指标数据库 EDB)
617
677
 
618
678
  ```bash
package/dist/src/cli.js CHANGED
@@ -3,7 +3,8 @@ import { Command, Option } from "commander";
3
3
  import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/asyncContent.js";
4
4
  import { readTokenCache } 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";
@@ -131,6 +132,7 @@ const announcement = new Command("announcement");
131
132
  const announcementHk = new Command("announcement-hk");
132
133
  const foreignOpinion = new Command("foreign-opinion");
133
134
  const independentOpinion = new Command("independent-opinion");
135
+ const officialAccount = new Command("official-account");
134
136
  addTimeFilters(opinion.command("list").option("--rank-type <number>", "Rank type", "1").option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
135
137
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
136
138
  rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
@@ -247,6 +249,16 @@ addTimeFilters(independentOpinion.command("list").option("--rank-type <number>",
247
249
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
248
250
  })));
249
251
  addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", required: true } });
252
+ addTimeFilters(officialAccount.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("--account-id <id>", "Official account ID", collectList, []).option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--category <type>", "Article type: news/law/report/view/data/event/meeting/notice/recruit/investEdu/brand/notes/other", collectList, []).option("--industry <id>", "Industry ID (constant-list citicIndustry/swIndustry)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.official-account.list", {
253
+ from: parseFrom(options.from), size: parseSize(options.size),
254
+ startTime: options.startTime, endTime: options.endTime,
255
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
256
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
257
+ keyword: options.keyword,
258
+ accountIdList: maybeArray(options.accountId), securityList: maybeArray(options.security),
259
+ categoryList: maybeArray(options.category), industryList: maybeArray(options.industry),
260
+ }), { endpointKey: "insight.official-account.list", idField: "articleId" }));
261
+ addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.download", idOption: "--article-id", idField: "articleId", fallbackPrefix: "official-account", fileType: { description: "File type: 1=txt(default) 2=HTML", default: "1" }, titleListEndpoint: "insight.official-account.list" });
250
262
  insight.addCommand(opinion);
251
263
  insight.addCommand(summary);
252
264
  insight.addCommand(roadshow);
@@ -259,6 +271,7 @@ insight.addCommand(announcement);
259
271
  insight.addCommand(announcementHk);
260
272
  insight.addCommand(foreignOpinion);
261
273
  insight.addCommand(independentOpinion);
274
+ insight.addCommand(officialAccount);
262
275
  program.addCommand(insight);
263
276
  const quote = new Command("quote").description("Quote APIs");
264
277
  const addKlineCommand = (name, endpointKey, securityHelp, shardDays) => quote.command(name)
@@ -421,7 +434,7 @@ addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "
421
434
  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))));
422
435
  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))));
423
436
  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", {})));
424
- vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; use 'all' for all pools", collectList, ["all"]).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", { poolIdList: options.poolId })));
437
+ 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))));
425
438
  program.addCommand(vault);
426
439
  program.addCommand(ai);
427
440
  const alternative = new Command("alternative").description("Alternative data APIs");
@@ -448,6 +461,23 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
448
461
  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 })));
449
462
  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 })));
450
463
  program.addCommand(alternative);
464
+ const indicator = new Command("indicator").description("Data indicator (EDE) APIs: search codes, cross-section, time-series");
465
+ 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) => {
466
+ const raw = await client.call("indicator.search", {
467
+ keyword: options.keyword,
468
+ limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
469
+ });
470
+ await printData(unwrapIndicatorData(raw), parseOutputFormat(options.format), options.output);
471
+ }));
472
+ 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) => {
473
+ const raw = await client.call("indicator.cross-section", buildIndicatorCrossSectionBody(options));
474
+ await printData(flattenCrossSection(unwrapIndicatorData(raw)), parseOutputFormat(options.format), options.output);
475
+ }));
476
+ 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) => {
477
+ const raw = await client.call("indicator.time-series", buildIndicatorTimeSeriesBody(options));
478
+ await printData(flattenTimeSeries(unwrapIndicatorData(raw)), parseOutputFormat(options.format), options.output);
479
+ }));
480
+ program.addCommand(indicator);
451
481
  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) => {
452
482
  const endpoint = ENDPOINTS[endpointKey];
453
483
  if (!endpoint) {
@@ -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
+ }
@@ -30,7 +30,12 @@ export function normalizeToken(token) {
30
30
  }
31
31
  export function requireAccessCredentials(accessKey, secretKey) {
32
32
  if (!accessKey || !secretKey) {
33
- throw new ConfigError("Missing GANGTISE_ACCESS_KEY or GANGTISE_SECRET_KEY");
33
+ const missing = [!accessKey && "GANGTISE_ACCESS_KEY", !secretKey && "GANGTISE_SECRET_KEY"].filter(Boolean).join(", ");
34
+ throw new ConfigError(`缺少环境变量: ${missing}(未导出到当前进程环境)\n`
35
+ + `注意:在 shell 里赋值还不够,必须"导出",子进程才读得到:\n`
36
+ + ` bash/zsh: export GANGTISE_ACCESS_KEY=... GANGTISE_SECRET_KEY=...\n`
37
+ + ` fish: set -gx GANGTISE_ACCESS_KEY ...; set -gx GANGTISE_SECRET_KEY ...\n`
38
+ + `验证:env | grep GANGTISE(能列出对应行才算导出成功)`);
34
39
  }
35
40
  return { accessKey, secretKey };
36
41
  }
@@ -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
+ }
@@ -161,6 +161,21 @@ export const ENDPOINTS = {
161
161
  kind: "download",
162
162
  description: "Download foreign independent opinion file",
163
163
  },
164
+ "insight.official-account.list": {
165
+ key: "insight.official-account.list",
166
+ method: "POST",
167
+ path: "/application/open-insight/officialAccount/getList",
168
+ kind: "json",
169
+ description: "List WeChat official account articles",
170
+ pagination: { enabled: true, maxPageSize: 50 },
171
+ },
172
+ "insight.official-account.download": {
173
+ key: "insight.official-account.download",
174
+ method: "GET",
175
+ path: "/application/open-insight/officialAccount/download/file",
176
+ kind: "download",
177
+ description: "Download WeChat official account article (txt/HTML)",
178
+ },
164
179
  // ─── reference ───
165
180
  "reference.securities-search": {
166
181
  key: "reference.securities-search",
@@ -544,4 +559,26 @@ export const ENDPOINTS = {
544
559
  kind: "json",
545
560
  description: "Query concept (theme index) constituent securities, grouped",
546
561
  },
562
+ // ─── indicator (EDE: security-level data indicators) ───
563
+ "indicator.search": {
564
+ key: "indicator.search",
565
+ method: "POST",
566
+ path: "/application/open-indicator/EDE/search",
567
+ kind: "json",
568
+ description: "Search data indicators by keyword (returns indicatorCode + params)",
569
+ },
570
+ "indicator.cross-section": {
571
+ key: "indicator.cross-section",
572
+ method: "POST",
573
+ path: "/application/open-indicator/EDE/cross-section",
574
+ kind: "json",
575
+ description: "Get cross-section data (multi-indicator x multi-security, single date)",
576
+ },
577
+ "indicator.time-series": {
578
+ key: "indicator.time-series",
579
+ method: "POST",
580
+ path: "/application/open-indicator/EDE/time-series",
581
+ kind: "json",
582
+ description: "Get time-series data (multi-indicator x single-security OR single-indicator x multi-security)",
583
+ },
547
584
  };
@@ -0,0 +1,95 @@
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 flat [numIndicators * numSecurities][1] array in
50
+ // indicator-major order, so indicator i on security j is values[i*numSec+j][0].
51
+ export function flattenCrossSection(data) {
52
+ if (!data || typeof data !== "object")
53
+ return data;
54
+ const d = data;
55
+ const securityCode = asStringArray(d.securityCode);
56
+ const indicators = asStringArray(d.indicators);
57
+ if (!Array.isArray(d.values) || !securityCode || !indicators)
58
+ return data;
59
+ const securityName = asStringArray(d.securityName);
60
+ const headers = buildHeaders(asStringArray(d.indicatorName), indicators, indicators.length);
61
+ const numSec = securityCode.length;
62
+ const list = securityCode.map((code, j) => {
63
+ const row = { date: d.date, security: code, name: securityName?.[j] };
64
+ for (let i = 0; i < indicators.length; i++) {
65
+ row[headers[i]] = rowOf(d.values, i * numSec + j)?.[0];
66
+ }
67
+ return row;
68
+ });
69
+ return { list, total: list.length };
70
+ }
71
+ // Time-series: one row per date. Columns are the indicators (single-security
72
+ // case) or the securities (single-indicator case) — exactly one dimension
73
+ // varies, per the API contract. `values` is a 2D [series][date] matrix.
74
+ export function flattenTimeSeries(data) {
75
+ if (!data || typeof data !== "object")
76
+ return data;
77
+ const d = data;
78
+ const dates = asStringArray(d.dates);
79
+ const securityCode = asStringArray(d.securityCode);
80
+ const indicators = asStringArray(d.indicators);
81
+ if (!Array.isArray(d.values) || !dates || !securityCode || !indicators)
82
+ return data;
83
+ const seriesAreIndicators = securityCode.length <= 1;
84
+ const headers = seriesAreIndicators
85
+ ? buildHeaders(asStringArray(d.indicatorName), indicators, indicators.length)
86
+ : buildHeaders(asStringArray(d.securityName), securityCode, securityCode.length);
87
+ const list = dates.map((date, k) => {
88
+ const row = { date };
89
+ for (let i = 0; i < headers.length; i++) {
90
+ row[headers[i]] = rowOf(d.values, i)?.[k];
91
+ }
92
+ return row;
93
+ });
94
+ return { list, total: list.length };
95
+ }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.17.2";
2
+ export const CLI_VERSION = "0.19.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.17.2",
3
+ "version": "0.19.0",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {