gangtise-openapi-cli 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { maybeArray, parseFrom, parseIndicatorParams, parseOptionalNumberOption, parseSize } from "./args.js";
1
+ import { maybeArray, parseFrom, parseIndicatorParams, parseOptionalNumberOption, parseScreenerIndicators, parseSize } from "./args.js";
2
2
  export function buildQuoteKlineBody(options) {
3
3
  return {
4
4
  securityList: maybeArray(options.security),
@@ -34,25 +34,71 @@ export function buildStockPoolStocksBody(options) {
34
34
  poolIdList: options.poolId?.length ? options.poolId : ["all"],
35
35
  };
36
36
  }
37
+ /** Parameter keys that REPLACE an indicator's `tradeDate`. The 2026-08-01 EDE
38
+ * revision dropped the root-level `date`, so `--date` now fans out into each
39
+ * indicator's `tradeDate`. Report-period indicators read `reportDate` instead
40
+ * and answer a `tradeDate` with an EMPTY result rather than an error (probed
41
+ * 2026-08-01 on `is_op_rev_mom`), so a caller-supplied `reportDate` suppresses
42
+ * the injection for that indicator. Injecting into an indicator that takes no
43
+ * date at all is harmless — `pty_op_scope` still answers normally with a stray
44
+ * `tradeDate` attached.
45
+ *
46
+ * `sDate` is deliberately NOT here. It is an interval START, not a substitute:
47
+ * `qte_vol_intvl` declares `tradeDate` required (the interval END) and `sDate`
48
+ * optional. Treating it as a replacement dropped `--date` and silently moved the
49
+ * interval end — probed 2026-08-02, 茅台 sDate=2024-01-02 returned 2,265,873,849
50
+ * without a tradeDate vs 65,687,435 with tradeDate=2024-01-31, both exit 0. */
51
+ const DATE_PARAM_KEYS = new Set(["tradeDate", "reportDate"]);
52
+ function withQueryDate(groups, codes, date) {
53
+ const merged = new Map();
54
+ for (const group of groups ?? [])
55
+ merged.set(group.indicatorCode, group);
56
+ for (const code of codes) {
57
+ const group = merged.get(code);
58
+ if (!group) {
59
+ merged.set(code, { indicatorCode: code, parameters: [{ paramKey: "tradeDate", paramValue: date }] });
60
+ }
61
+ else if (!group.parameters.some((param) => DATE_PARAM_KEYS.has(param.paramKey))) {
62
+ group.parameters.push({ paramKey: "tradeDate", paramValue: date });
63
+ }
64
+ }
65
+ return [...merged.values()];
66
+ }
37
67
  export function buildIndicatorCrossSectionBody(options) {
38
68
  return {
39
69
  indicatorCodeList: maybeArray(options.indicator),
40
- securityCodeList: maybeArray(options.security),
41
- date: options.date,
70
+ universe: maybeArray(options.security),
42
71
  currency: options.currency,
43
72
  scale: options.scale,
44
- indicatorParamList: parseIndicatorParams(options.indicatorParam),
73
+ indicatorParamList: withQueryDate(parseIndicatorParams(options.indicatorParam), options.indicator, options.date),
45
74
  };
46
75
  }
47
76
  export function buildIndicatorTimeSeriesBody(options) {
48
77
  return {
49
78
  indicatorCodeList: maybeArray(options.indicator),
50
- securityCodeList: maybeArray(options.security),
79
+ universe: maybeArray(options.security),
51
80
  startDate: options.startDate,
52
81
  endDate: options.endDate,
53
82
  calendarType: options.calendarType,
54
83
  currency: options.currency,
55
84
  scale: options.scale,
56
- indicatorParamList: parseIndicatorParams(options.indicatorParam),
85
+ // The endpoint requires the key even with nothing to configure.
86
+ indicatorParamList: parseIndicatorParams(options.indicatorParam) ?? [],
87
+ };
88
+ }
89
+ export function buildIndicatorScreenerBody(options) {
90
+ const indicators = parseScreenerIndicators(options.indicator, options.indicatorParam, options.expression);
91
+ return {
92
+ universe: maybeArray(options.security),
93
+ expression: options.expression,
94
+ // Every indicator gets a date, including ones whose parameterList is empty.
95
+ // That is not just convenience: the screener drops any indicator sent with
96
+ // `parameters: []` — probed 2026-08-02, `pty_op_scope` + `F1 contains '酒'`
97
+ // matches nothing with an empty list and correctly returns 五粮液/贵州茅台
98
+ // with a (for that indicator meaningless) tradeDate attached. The official
99
+ // doc's own `F3 contains '酒'` example cannot work as written.
100
+ indicatorList: indicators.map((indicator) => (indicator.parameters.some((param) => DATE_PARAM_KEYS.has(param.paramKey))
101
+ ? indicator
102
+ : { ...indicator, parameters: [...indicator.parameters, { paramKey: "tradeDate", paramValue: options.date }] })),
57
103
  };
58
104
  }
@@ -632,6 +632,15 @@ const ENDPOINT_DEFS = {
632
632
  description: "Get time-series data (multi-indicator x single-security OR single-indicator x multi-security)",
633
633
  retry: "no-999999",
634
634
  },
635
+ // Note the path: the screener sits directly under open-indicator, NOT under
636
+ // the EDE/ prefix its three siblings share.
637
+ "indicator.screener": {
638
+ method: "POST",
639
+ path: "/application/open-indicator/screener",
640
+ kind: "json",
641
+ description: "Screen securities by an expression over indicator values (条件选股)",
642
+ retry: "no-999999",
643
+ },
635
644
  // ─── tool (open-tool: async file parsing) ───
636
645
  "tool.file-parse.submit": {
637
646
  method: "POST",
@@ -83,7 +83,10 @@ const ERROR_HINTS = {
83
83
  "130004": "下载 ID 需为数字,检查该命令的 --*-id 参数是否传对。",
84
84
  "130005": "对照命令 --help 检查 --file-type / --content-type 取值。",
85
85
  "140001": "稍后用对应 *-check 命令查询。",
86
- "140002": "异步生成失败(终态)——换参数重新提交,重试同一 dataId 不会变。",
86
+ // 两类来源共用此码:AI 异步生成失败,以及 EDE 的参数/表达式错误(实测 2026-08-02:
87
+ // 枚举越界「参数 adjustType 的值 99 不在有效范围内 [1,2,3,4]」、表达式语法错误)。
88
+ // 两者都是终态、都不该重试,所以文案只说「换参数重来」而不预设是哪一类。
89
+ "140002": "终态失败,重试同一请求不会变——按 msg 改参数后重新提交;EDE 的指标参数名/枚举以 indicator search 的 parameterList 为准。",
87
90
  // ── 接口专有 2xxxxx ──
88
91
  "210001": "换一篇,或改用 list 取正文摘要。",
89
92
  "220001": "改用 list 取正文摘要。",
@@ -98,10 +101,11 @@ const ERROR_HINTS = {
98
101
  "900001": "对照命令 --help 检查必填项。",
99
102
  "900002": "请求方法不正确(服务端 msg 为「请求类型有误」)——`raw call` 时检查该 endpoint 是 GET 还是 POST。",
100
103
  "903301": "次日再试,或联系客户经理提额。",
101
- // EDE 专有旧码,未被 2026-07-17 重排收编但仍是 indicator 取数的主要报错
102
- // (references/commands/indicator.md 把这两个列为首要排查项)。
104
+ // EDE 专有旧码。2026-08-02 复测已不再出现——入参/表达式错走 100003,指标必填
105
+ // 参数缺失与枚举越界走 140002。保留兜底:错误码是按模块分批迁移的,回滚或未迁完
106
+ // 的路径仍可能吐旧码,届时给条能用的提示比 fallthrough 强。
103
107
  "410001": "补齐 --indicator / --security;`time-series` 不支持「多指标 × 多证券」,改用 `indicator cross-section`。",
104
- "410106": "读 `indicator search --format json` 的 parameterList,用 --indicator-param 补上 required:true 的参数(如 periodNum / startDate / fiscalYear)。",
108
+ "410106": "读 `indicator search --format json` 的 parameterList,用 --indicator-param 补上 required:true 的参数(如 periodNum / sDate / fiscalYear)。",
105
109
  "410004": "换证券或日期确认该条件下本应有数据;仍失败多为未开通该指标,联系客户经理。",
106
110
  "410110": "稍后用对应 *-check 命令查询。",
107
111
  "410111": "终态,换参数后重新提交,重试同一请求不会变。",
@@ -31,23 +31,32 @@ export function unwrapIndicatorData(raw) {
31
31
  function asStringArray(value) {
32
32
  return Array.isArray(value) ? value.map((item) => String(item)) : undefined;
33
33
  }
34
+ function asIndicatorMetaList(value) {
35
+ if (!Array.isArray(value))
36
+ return undefined;
37
+ return value.map((item) => (item && typeof item === "object" && !Array.isArray(item) ? item : {}));
38
+ }
39
+ function optionalString(value) {
40
+ return value === undefined || value === null ? undefined : String(value);
41
+ }
34
42
  function rowOf(values, index) {
35
43
  const row = values[index];
36
44
  return Array.isArray(row) ? row : undefined;
37
45
  }
38
46
  // Build one column header per series. Prefer the human-readable name; on a
39
- // duplicate name append the code so a column is never silently overwritten.
40
- function buildHeaders(names, codes, count) {
41
- // Pre-seed the metadata column names: an indicator literally named "date" /
42
- // "security" / "name" must get a suffixed header, not overwrite the metadata.
43
- const used = new Set(["date", "security", "name"]);
47
+ // duplicate name append a disambiguator so a column is never silently
48
+ // overwritten. `reserved` pre-seeds the metadata column names, so a series
49
+ // literally named "date" / "security" / "name" gets suffixed rather than
50
+ // clobbering the metadata.
51
+ function buildHeaders(bases, suffixes, reserved) {
52
+ const used = new Set(reserved);
44
53
  const headers = [];
45
- for (let i = 0; i < count; i++) {
46
- const base = String(names?.[i] ?? codes?.[i] ?? `col${i}`);
54
+ for (let i = 0; i < bases.length; i++) {
55
+ const base = bases[i] ?? suffixes[i] ?? `col${i}`;
47
56
  let header = base;
48
57
  let attempt = 1;
49
58
  while (used.has(header)) {
50
- const suffix = codes?.[i] ?? i;
59
+ const suffix = suffixes[i] ?? i;
51
60
  header = attempt === 1 ? `${base} (${suffix})` : `${base} (${suffix})_${attempt}`;
52
61
  attempt++;
53
62
  }
@@ -56,27 +65,85 @@ function buildHeaders(names, codes, count) {
56
65
  }
57
66
  return headers;
58
67
  }
59
- // Cross-section: one row per security, one column per indicator. The live
60
- // `values` is a 2D [numIndicators][numSecurities] matrix in indicator-major
61
- // order, so indicator i on security j is values[i][j].
62
- // keyBy "code" makes each indicator column its `indicatorCode` (unique, and
63
- // independent of the display name or the server's column order) instead of the
64
- // human name — required for batch code→value mapping, where names collide
65
- // (many indicators share a display name) and the server reorders columns.
68
+ // Column headers for a list of indicator metadata. keyBy "code" makes each
69
+ // column its `code` (unique, and independent of the display name or the
70
+ // server's column order) instead of the human name — required for batch
71
+ // code→value mapping, where names collide (many indicators share a display
72
+ // name) and the server reorders columns relative to the request. The screener
73
+ // disambiguates by `field` instead, because there the SAME code may legitimately
74
+ // appear twice under two variables with different parameters.
75
+ function indicatorHeaders(indicators, keyBy, reserved) {
76
+ const codes = indicators.map((meta) => optionalString(meta.code));
77
+ const fields = indicators.map((meta, i) => optionalString(meta.field) ?? codes[i]);
78
+ const bases = keyBy === "code" ? codes : indicators.map((meta) => optionalString(meta.name));
79
+ // On a screener payload every column carries its variable, so a repeated base
80
+ // can suffix ALL of its occurrences instead of leaving the first one bare.
81
+ // A bare `收盘价` next to `收盘价 (F2)` reads as "the" close price when it is
82
+ // really just whichever variable the server happened to list first.
83
+ const screener = indicators.some((meta) => meta.field !== undefined);
84
+ if (!screener)
85
+ return buildHeaders(bases, fields, reserved);
86
+ const repeated = new Set(bases.filter((base, i) => base !== undefined && bases.indexOf(base) !== i));
87
+ return buildHeaders(bases.map((base, i) => (base !== undefined && repeated.has(base) ? `${base} (${fields[i] ?? i})` : base)), fields, reserved);
88
+ }
89
+ const CROSS_SECTION_COLUMNS = ["security", "name"];
90
+ /** What the caller asked for that the response does not contain. The server
91
+ * does NOT pad missing data with `null`: an indicator that is empty for every
92
+ * security disappears from `indicatorList`, and a security that is empty for
93
+ * every indicator disappears from `securityCodeList` (probed 2026-08-02 — three
94
+ * indicators over 09992.HK came back with one, two securities over
95
+ * `qte_mkt_cptl` came back with one). Only partial gaps become `null`.
96
+ *
97
+ * That is the dangerous shape: `--key-by code` batch mapping finds no key at
98
+ * all rather than a null, and a cross-market pull quietly returns fewer rows
99
+ * than requested with exit code 0.
100
+ *
101
+ * Universe entries with no `.` are skipped — those are sector IDs, which the
102
+ * server expands into constituents, so their absence from the response is
103
+ * expected rather than a dropped row. */
104
+ export function droppedFromMatrix(data, requestedSecurities, requestedIndicators) {
105
+ const empty = { securities: [], indicators: [] };
106
+ if (!data || typeof data !== "object")
107
+ return empty;
108
+ const d = data;
109
+ const returnedSecurities = new Set(asStringArray(d.securityCodeList) ?? []);
110
+ const returnedIndicators = new Set((asIndicatorMetaList(d.indicatorList) ?? []).map((meta) => optionalString(meta.code)));
111
+ return {
112
+ securities: requestedSecurities.filter((code) => code.includes(".") && !returnedSecurities.has(code)),
113
+ indicators: requestedIndicators.filter((code) => !returnedIndicators.has(code)),
114
+ };
115
+ }
116
+ /** The matrix and its axis labels must agree, or every cell is off by a row.
117
+ * The 2026-08-01 revision transposed cross-section without a version marker, so
118
+ * a future re-transpose has to fail loudly instead of silently relabelling
119
+ * columns. Rows are compared, not cells: the server legitimately returns short
120
+ * rows when a trailing indicator has no data anywhere. */
121
+ function assertRowCount(rows, expected, axis) {
122
+ if (rows.length !== expected) {
123
+ throw new ApiError(`Indicator matrix shape mismatch: got ${rows.length} value rows for ${expected} ${axis} — the response layout may have changed`, undefined);
124
+ }
125
+ }
126
+ // Cross-section (and the screener, whose payload is the same shape plus a
127
+ // per-indicator `field`): one row per security, one column per indicator.
128
+ // `values` is [security][indicator], so security i's value for indicator j is
129
+ // values[i][j]. There is no row-level date any more — the query date now lives
130
+ // in each indicator's own parameters and may legitimately differ per column.
66
131
  export function flattenCrossSection(data, keyBy = "name") {
67
132
  if (!data || typeof data !== "object")
68
133
  return data;
69
134
  const d = data;
70
135
  const securityCode = asStringArray(d.securityCodeList);
71
- const indicators = asStringArray(d.indicatorCodeList);
136
+ const indicators = asIndicatorMetaList(d.indicatorList);
72
137
  if (!Array.isArray(d.values) || !securityCode || !indicators)
73
138
  return data;
139
+ assertRowCount(d.values, securityCode.length, "securities");
74
140
  const securityName = asStringArray(d.securityNameList);
75
- const headers = buildHeaders(keyBy === "code" ? indicators : asStringArray(d.indicatorNameList), indicators, indicators.length);
76
- const list = securityCode.map((code, j) => {
77
- const row = { date: d.date, security: code, name: securityName?.[j] };
78
- for (let i = 0; i < indicators.length; i++) {
79
- row[headers[i]] = rowOf(d.values, i)?.[j];
141
+ const headers = indicatorHeaders(indicators, keyBy, CROSS_SECTION_COLUMNS);
142
+ const list = securityCode.map((code, i) => {
143
+ const row = { security: code, name: securityName?.[i] };
144
+ const cells = rowOf(d.values, i);
145
+ for (let j = 0; j < headers.length; j++) {
146
+ row[headers[j]] = cells?.[j];
80
147
  }
81
148
  return row;
82
149
  });
@@ -84,20 +151,32 @@ export function flattenCrossSection(data, keyBy = "name") {
84
151
  }
85
152
  // Time-series: one row per date. Columns are the indicators (single-security
86
153
  // case) or the securities (single-indicator case) — exactly one dimension
87
- // varies, per the API contract. `values` is a 2D [series][date] matrix.
88
- export function flattenTimeSeries(data, keyBy = "name") {
154
+ // varies, per the API contract. `values` stays a 2D [series][date] matrix.
155
+ //
156
+ // `requestedSecurities` decides the axis. Deriving it from the RESPONSE is
157
+ // wrong: the server drops securities that have no data at all, so a
158
+ // single-indicator × two-security query where one security is uncovered comes
159
+ // back with one security and silently relabels the column as the indicator —
160
+ // the caller then cannot tell whose series they are holding (probed 2026-08-02:
161
+ // `finc_pe_ttm` over 600519.SH + 09992.HK renders a bare 市盈率(TTM) column).
162
+ export function flattenTimeSeries(data, keyBy = "name", requestedSecurities) {
89
163
  if (!data || typeof data !== "object")
90
164
  return data;
91
165
  const d = data;
92
166
  const dates = asStringArray(d.dates);
93
167
  const securityCode = asStringArray(d.securityCodeList);
94
- const indicators = asStringArray(d.indicatorCodeList);
168
+ const indicators = asIndicatorMetaList(d.indicatorList);
95
169
  if (!Array.isArray(d.values) || !dates || !securityCode || !indicators)
96
170
  return data;
97
- const seriesAreIndicators = securityCode.length <= 1;
171
+ const securityName = asStringArray(d.securityNameList);
172
+ const seriesAreIndicators = (requestedSecurities ?? securityCode.length) <= 1;
173
+ assertRowCount(d.values, seriesAreIndicators ? indicators.length : securityCode.length, seriesAreIndicators ? "indicators" : "securities");
174
+ // Map over securityCode rather than securityNameList directly: a response that
175
+ // omits the names must still yield one header per security (falling back to
176
+ // the code), not zero columns.
98
177
  const headers = seriesAreIndicators
99
- ? buildHeaders(keyBy === "code" ? indicators : asStringArray(d.indicatorNameList), indicators, indicators.length)
100
- : buildHeaders(keyBy === "code" ? securityCode : asStringArray(d.securityNameList), securityCode, securityCode.length);
178
+ ? indicatorHeaders(indicators, keyBy, ["date"])
179
+ : buildHeaders(securityCode.map((code, i) => (keyBy === "code" ? code : securityName?.[i])), securityCode, ["date"]);
101
180
  const list = dates.map((date, k) => {
102
181
  const row = { date };
103
182
  for (let i = 0; i < headers.length; i++) {
@@ -104,11 +104,12 @@ const RETRYABLE_API_CODES = new Set(["999999"]);
104
104
  // - 140002 PROCESSING_FAILED: the async *-check endpoints (get-content) declare no
105
105
  // retry policy, so a 140002@500 would be retried 2× by the default policy BEFORE
106
106
  // asyncContent's FAILED_CODES gets to call it terminal — that guard sits above
107
- // client.call's withRetry and cannot see the retries. 140002 means "generation
108
- // failed" (terminal by definition) and only those async endpoints can return it,
109
- // so a blanket rule is safe and skips the wasted retries. Server still emits the
110
- // legacy 410111 today (410111@400 isn't retryable anyway); this is a forward guard
111
- // for the documented 140002@500.
107
+ // client.call's withRetry and cannot see the retries. 140002 is terminal in both
108
+ // of its meanings — async "generation failed", and (probed 2026-08-02) the EDE
109
+ // indicator endpoints' parameter/expression errors, e.g. an out-of-range enum or
110
+ // a malformed screener expression — so the blanket rule stays correct and skips
111
+ // the wasted retries. Server still emits the legacy 410111 today (410111@400
112
+ // isn't retryable anyway); this is a forward guard for the documented 140002@500.
112
113
  const TERMINAL_API_CODES = new Set(["999011", "140002"]);
113
114
  // Connect-phase / DNS failures: the request provably never reached the server, so a
114
115
  // replay cannot double-execute (or double-bill) anything even under "no-replay".
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.29.0";
2
+ export const CLI_VERSION = "0.30.1";
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: gangtise-openapi
3
- version: "0.29.0"
3
+ version: "0.30.1"
4
4
  description: |-
5
5
  通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
6
6
 
7
7
  **触发词**:调接口 / CLI / openapi / 导出 / 下载研报 / 批量查 / 拉数据 / 跑一下 / 钢尼斯 / gtIC(Gangtise 语音误识别)
8
8
 
9
- **适用**:原始数据导出、批量 jsonl/csv、下载 PDF/MD、行情 K 线、财务报表、估值指标、证券级数据指标(EDE 截面/时序)、财报日历(业绩预告/快报/公告)、PDF 解析为 Markdown、AI 能力(一页通/投资逻辑/同业对比/个股看点·投研总结/投研线索/业绩点评/观点PK·多空辩论/主题跟踪/热点话题/管理层讨论/调研提纲/知识库搜索)、云盘文件管理(Vault)
9
+ **适用**:原始数据导出、批量 jsonl/csv、下载 PDF/MD、行情 K 线、财务报表、估值指标、证券级数据指标(EDE 截面/时序/条件选股)、财报日历(业绩预告/快报/公告)、PDF 解析为 Markdown、AI 能力(一页通/投资逻辑/同业对比/个股看点·投研总结/投研线索/业绩点评/观点PK·多空辩论/主题跟踪/热点话题/管理层讨论/调研提纲/知识库搜索)、云盘文件管理(Vault)
10
10
 
11
11
  **不适用**:不脱离 OpenAPI 自行撰写研报、编造投研结论或做自由问答——观点总结、多空 PK 等 AI 产物本 skill 只经由 Gangtise 平台 AI 接口获取,不自行生成
12
12
 
@@ -32,7 +32,7 @@ description: |-
32
32
  7. **CLI 已内置自动化,不要手动复刻**:
33
33
  - 翻页 → 首页拿 total 后剩余页并发拉取
34
34
  - K 线 `--security all` 跨日期 → 自动按日切片并合并
35
- - 5xx / `429` / 网络错误 / `999999` → 自动指数退避重试(🔴 贵档端点例外:仅连接失败 / 429 / token 自愈重试,5xx/超时不重放防重复扣分,v0.26.0;`indicator` 端点对 `999999` 不重试——该码=查询无数据,v0.27.0)
35
+ - 5xx / `429` / 网络错误 / `999999` → 自动指数退避重试(🔴 贵档端点例外:仅连接失败 / 429 / token 自愈重试,5xx/超时不重放防重复扣分,v0.26.0;`indicator` 端点对 `999999` 不重试,v0.27.0)
36
36
  - Token 失效(`0000001008` / `999002`,含已废弃的 `8000014`/`8000015`)→ 自动重新登录并重试一次;凭证错 `999011` → **不重试**(AK/SK 不对不会自己好),查环境变量
37
37
  8. **参数命名差异**:Insight/Quote/Vault 用 `--security`,Fundamental/AI 用 `--security-code`(例外:`ai stock-summary` 用 `--security`,`ai security-clue` 用 `--gts-code`)。
38
38
  9. **调试**:`--verbose` 或 `GANGTISE_VERBOSE=1` 打印每个请求的耗时/字节数到 stderr。
@@ -75,7 +75,7 @@ description: |-
75
75
  - 🔴 **按次贵**:`ai knowledge-batch` 10、`management-discuss-*` 10;AI Agent(`one-pager` / `investment-logic` / `peer-comparison` / `research-outline` / `earnings-review` / `viewpoint-debate` / `theme-tracking`)**50/次**;`ai hot-topic` 50/篇
76
76
  - 🔴 **极贵**:`alternative concept-info` / `concept-securities` **500/次**
77
77
  - ⚠️ **同参数重复调用不免费**:按次计费无缓存命中豁免(2026-07-11 实测 `one-pager` 重复调用每次扣分,即使秒回缓存内容)——生成类结果拿到后自行留存复用,别为"刷新"重调;CLI 已对上述 🔴 贵档端点关闭 5xx/超时自动重放(v0.26.0),50/篇 的 `summary` / `foreign-report` / `my-conference` download 同样不重放(v0.27.0),正是为防重复扣分
78
- - **按单元格**:`indicator cross-section` / `time-series`(A股 0.05 / 港股 0.1 / 美股 0.2 积分每 100 单元格,见 `indicator.md`);`ai knowledge-resource-download` 按下游资源计费
78
+ - **按单元格**:`indicator cross-section` / `time-series` / `screener`(A股 0.05 / 港股 0.1 / 美股 0.2 积分每 100 单元格;screener 按**筛选前**范围计费,见 `indicator.md`);`ai knowledge-resource-download` 按下游资源计费
79
79
 
80
80
  ### 下载规则(`--file-type` / `--content-type`)
81
81
 
@@ -149,6 +149,7 @@ description: |-
149
149
  | 多证券已实现财务 / 估值指标搜索(含总市值) | `indicator search` |
150
150
  | 多证券已实现指标截面(多指标 × 多证券,同一查询日期) | `indicator cross-section`(前置:`indicator search --format json` 通过三项校验) |
151
151
  | 多证券已实现指标时序(单指标 × 多证券,按区间) | `indicator time-series`(前置:`indicator search --format json` 通过三项校验) |
152
+ | 条件选股 / 按指标筛股票(市值+PE+经营范围等多条件组合) | `indicator screener`(前置:`indicator search` 拿 code;范围可传板块 ID,见 `reference sector-search`) |
152
153
  | 证券代码 / gtsCode 搜索 | `reference securities-search` |
153
154
  | 首席 ID / 分析师 ID 搜索 | `reference chiefs-search`(按姓名/机构/团队,用于 `insight opinion --chief`) |
154
155
  | 机构 ID 搜索(内资券商/外资/牵头/观点机构) | `reference institution-search`(按机构名,用于 `--institution` / `--broker`;免费) |
@@ -171,7 +172,8 @@ description: |-
171
172
  - EDE 取数前必须用 `search --format json` 同时核对:`indicatorName` + `description` 语义准确、`scopeList` 覆盖全部目标市场 / 证券类型、`parameterList` 必填参数与枚举可满足;`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退专用接口。专用接口也不覆盖目标市场时,说明当前不可用,不要硬调。`scopeList` 按指标各不相同,不能因 EDE 服务支持 A / 港 / 美股就假定某个指标三市场都覆盖
172
173
  - `indicator search` 免费,`cross-section` / `time-series` 按单元格计费;除多证券批量的效率收益外,仍优先免费 / 低价的 `quote` 或 `fundamental`
173
174
  - 行业 / 宏观指标(空调销量、社融等,无证券维度)走 `alternative edb-*`(EDB),不要与证券级 EDE 混用
174
- - EDE 单元格级缺值返回 `null` 且保留证券行;**整个查询无数据仍可能报 `999999`**。日期语义按指标分三类:财务报表指标=报告期末(可为非交易日)、`finc_pe_ttm` 等日频估值=最新交易日、`finc_pb_mrq`(MRQ) 等=最近报告期末(交易日取 `null`);混合取数按各自有效日期分次 `cross-section` 再按证券合并,别塞进同一个 `--date`。详见 `references/commands/indicator.md`
175
+ - EDE 缺数据分三档,**别只认 `null`**:部分缺 → 单元格 `null`(行列都在);某指标对所有证券都无数据 → **整列消失**;某证券对所有指标都无数据 → **整行消失**;整个查询无数据 → **空表**(2026-08-01 起不再报 `999999`)。后三种退出码都是 0,`--key-by code` 回填时 key 会直接不存在。CLI 已在 stderr 提示被整个略过的指标/证券。**参数写错也表现为空表**,拿到空表先查参数名和日期语义,别当成真没数据。日期语义按指标分三类:财务报表指标=报告期末(可为非交易日)、`finc_pe_ttm` 等日频估值=最新交易日、`finc_pb_mrq`(MRQ) 等=最近报告期末(交易日取 `null`);混合取数按各自有效日期分次 `cross-section` 再按证券合并,别塞进同一个 `--date`。详见 `references/commands/indicator.md`
176
+ - **EDE 指标参数名一律以 `indicator search --format json` 的 `parameterList` 为准**,不要凭记忆或照抄示例:服务端会改参数名(`adjustmentType` → `adjustType`,2026-08-01),**传错名是静默忽略、退回默认值**而不是报错——实测 `adjustmentType=3` 返回的是不复权价,看着正常实则错数据。`--security` 支持板块 ID(`reference sector-search` 的 10 位 `sectorId`;`--indicator` 只收指标编码)。⚠️ 根级 `--scale` 会污染不声明 `scale` 的指标(`qte_close` 被 `--scale 8` 缩成 `0`),价格与金额混查时改用 `--indicator-param "code:scale=8"`
175
177
  - "业绩点评"双义消歧:**检索已有**(研报/纪要里的业绩点评内容)走 `insight ... list --llm-tag earningsReview`(0.1/条);**AI 现生成**一份走 `ai earnings-review`(异步、50/次)。不确定问一句
176
178
 
177
179
  ## 公司名 → 证券代码
@@ -262,7 +264,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
262
264
  | 错误码 | 含义 | CLI 行为 | Agent 是否介入 |
263
265
  |--------|------|---------|--------------|
264
266
  | ✅ `100003` | 参数值非法——**最宽的兜底码**:类型错、`limit` 越界都归这里。**msg 通常已指明字段**(如「请求体字段类型不匹配: size 期望类型 Integer」「limit 最小为 1,最大为 10000」),先读 msg 再猜 | — | 按 msg 指的字段改;msg 没指明才对照 `--help` 查枚举拼写,**不要重试同命令** |
265
- | ✅ `999999` | 系统错误;但 **`indicator`(EDE)用此码 + HTTP 500 表示查询无数据**(节假日 / 未来日期 / 未覆盖标的,2026-07-11 实测)——单元格级缺值才是 `null` | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | `indicator` 遇到先检查日期/标的是否该有数据,别盲目重试 |
267
+ | ✅ `999999` | 系统错误。**`indicator`(EDE)2026-08-01 前用此码表示查询无数据,现已改为返回空数组**,所以这个码基本只剩真故障 | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
266
268
  | ✅ `410110` | **异步生成中**(HTTP 400,旧码未切)。新码 `140001`,CLI 两码都认 | 轮询视为 pending | 继续等 |
267
269
  | ✅ `410111` | **异步生成失败**(HTTP 400,旧码未切)。新码 `140002`,CLI 两码都认 | 终态 | **不重试**,换参数 |
268
270
  | ✅ `130002` | 资源不存在——**下载类的兜底码**:`reportId` 不存在 / 非数字 / `fileType` 非法**全归这里**(`130003`/`130004`/`130005` 实测均未启用) | — | 确认 ID 有效且本账号可见;换 `--file-type` 或换一篇验证 |
@@ -278,7 +280,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
278
280
  | ✅ `0000001008` | Token 服务端失效(他处登录挤掉)——**旧码未切,token 自愈依赖它** | **强制重新登录并重试一次** | 无 AK/SK 时无法自愈,提示重新登录 |
279
281
  | ✅ `0000001007` | 请求未携带 Bearer token | — | 检查 `GANGTISE_TOKEN` / AK/SK 是否已 export |
280
282
  | ✅ `900002` | **请求方法不正确**(msg「请求类型有误」,HTTP 405)——旧文档写作"缺少 uid"是错的 | — | `raw call` 时确认该 endpoint 是 GET 还是 POST |
281
- | `410106` / 缺参 | `indicator` 缺必填参数(msg 直接指明,如「必填参数 periodNum 不能为空」;HTTP 500 故 CLI 重试 ×2) | **自动重试 ×2** | 读 `indicator search --format json` 的 `parameterList` 补 `required:true` 参数 |
283
+ | ✅ `140002` | 终态参数错:AI 异步生成失败,或 `indicator` 的指标必填参数缺失 / 枚举越界 / 表达式语法错(实测 2026-08-02) | **不重试**(终态码) | 按 msg 改参数重提;EDE 的参数名与枚举读 `indicator search --format json` 的 `parameterList` |
282
284
 
283
285
  **⚠️ 实测发现的坑(都是"不报错"型,最难发现)**
284
286
  - 🔴 **日期只写 `YYYY-MM-DD`、时间只写 `YYYY-MM-DD HH:mm:ss`(或 10/13 位时间戳);CLI v0.28.0 起 date 与 datetime 两类、含所有 insight/vault 透传参数都本地拦截**。服务端对「年在后」格式**日月顺序随分隔符翻转**且静默误解析(HTTP 200、不回显实际用的日期):`07/01/2026`(斜杠)读成 **2026-01-07**、`07-01-2026`(横杠)读成 **2026-07-01**,差半年。实测 `insight research list --start-time`:`07/01/2026` 命中 1562 条、`07-01-2026` 命中 210 条(分别 = 标准 `2026-01-07` / `2026-07-01`);`quote day-kline`/`kline-hk`/`kline-us`/`index`、`fundamental balance-sheet` 同理。v0.28.0 前透传命令(research/summary/announcement-hk/us/vault/minute-kline 等)**静默放行**,且同值在本地转时间戳的 `announcement`(A 股)与透传的 hk/us 之间还会差半年、都 exit 0。现在全部在发请求前报 `ValidationError`,**但绕过 CLI 直连接口务必自己保证格式**
@@ -372,7 +374,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
372
374
  - knowledge-batch / security-clue / 个股看点(stock-summary)/ AI agent / 异步任务 / 主题跟踪 / 热点 / 管理层讨论 → `references/commands/ai.md`
373
375
  - drive / record / my-conference / wechat / 股票池 → `references/commands/vault.md`
374
376
  - 行业指标数据库(EDB)/ 题材指数画像与成分股(concept-info / concept-securities)→ `references/commands/alternative.md`
375
- - 数据指标(EDE:search / cross-section / time-series,证券级指标截面与时序)→ `references/commands/indicator.md`
377
+ - 数据指标(EDE:search / cross-section / time-series / screener,证券级指标截面、时序与条件选股)→ `references/commands/indicator.md`
376
378
  - PDF 解析(file-parse:上传 PDF → Markdown + 图片 ZIP)→ `references/commands/tool.md`
377
379
  - securities-search / chiefs-search(首席 ID)/ institution-search(机构 ID)/ official-account-search(公众号 ID)/ 常量查询(constant-category / constant-list)/ 题材 ID(concept-search)/ 板块(sector-search / sector-constituents)/ lookup 本地表 / 行业别名 / raw call → `references/commands/reference-and-lookup.md`
378
380