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.
- package/README.md +49 -197
- package/dist/src/cli.js +45 -8
- package/dist/src/core/args.js +85 -15
- package/dist/src/core/client.js +7 -5
- package/dist/src/core/commandBodies.js +52 -6
- package/dist/src/core/endpoints.js +9 -0
- package/dist/src/core/errors.js +8 -4
- package/dist/src/core/indicatorMatrix.js +106 -27
- package/dist/src/core/transport.js +6 -5
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +10 -8
- package/gangtise-openapi/references/commands/indicator.md +109 -40
- package/gangtise-openapi/references/examples.md +2 -2
- package/gangtise-openapi/references/response-schema.md +4 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
102
|
-
//
|
|
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 /
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 <
|
|
46
|
-
const base =
|
|
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 =
|
|
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
|
-
//
|
|
60
|
-
// `
|
|
61
|
-
// order
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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 =
|
|
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 =
|
|
76
|
-
const list = securityCode.map((code,
|
|
77
|
-
const row = {
|
|
78
|
-
|
|
79
|
-
|
|
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`
|
|
88
|
-
|
|
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 =
|
|
168
|
+
const indicators = asIndicatorMetaList(d.indicatorList);
|
|
95
169
|
if (!Array.isArray(d.values) || !dates || !securityCode || !indicators)
|
|
96
170
|
return data;
|
|
97
|
-
const
|
|
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
|
-
?
|
|
100
|
-
: buildHeaders(keyBy === "code" ?
|
|
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
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
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".
|
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.30.1";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gangtise-openapi
|
|
3
|
-
version: "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
|
|
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`
|
|
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
|
|
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
|
|
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` |
|
|
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
|
-
| `
|
|
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
|
|
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
|
|