gangtise-openapi-cli 0.30.1 → 0.32.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 +17 -8
- package/dist/src/cli.js +216 -56
- package/dist/src/core/args.js +95 -17
- package/dist/src/core/client.js +4 -3
- package/dist/src/core/commandBodies.js +20 -11
- package/dist/src/core/endpoints.js +18 -0
- package/dist/src/core/errors.js +10 -3
- package/dist/src/core/indicatorMatrix.js +312 -48
- package/dist/src/core/printer.js +5 -3
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +41 -16
- package/gangtise-openapi/references/commands/indicator.md +121 -38
- package/gangtise-openapi/references/commands/insight.md +51 -1
- package/gangtise-openapi/references/commands/quote.md +1 -1
- package/gangtise-openapi/references/commands/reference-and-lookup.md +4 -4
- package/gangtise-openapi/references/commands/vault.md +2 -2
- package/gangtise-openapi/references/examples.md +36 -12
- package/gangtise-openapi/references/lookup-ids.md +2 -2
- package/gangtise-openapi/references/response-schema.md +6 -2
- package/package.json +1 -1
package/dist/src/core/args.js
CHANGED
|
@@ -288,6 +288,100 @@ const SCREENER_FIELD = /^F[1-9][0-9]*$/;
|
|
|
288
288
|
* `contains 'F2 系列'` operand is not mistaken for a reference. */
|
|
289
289
|
const SCREENER_FIELD_REF = /\bF[1-9][0-9]*\b/g;
|
|
290
290
|
const SCREENER_STRING_LITERAL = /'[^']*'|"[^"]*"/g;
|
|
291
|
+
/** Split an expression into `(`, `)`, `&&`, `||` and the atoms between them.
|
|
292
|
+
* String literals are copied verbatim so a `||` or an `F2` inside one is never
|
|
293
|
+
* mistaken for syntax. */
|
|
294
|
+
function tokenizeScreenerExpression(src) {
|
|
295
|
+
const tokens = [];
|
|
296
|
+
let atom = "";
|
|
297
|
+
const flush = () => {
|
|
298
|
+
if (atom.trim())
|
|
299
|
+
tokens.push(atom);
|
|
300
|
+
atom = "";
|
|
301
|
+
};
|
|
302
|
+
for (let i = 0; i < src.length;) {
|
|
303
|
+
const ch = src[i];
|
|
304
|
+
if (ch === "'" || ch === '"') {
|
|
305
|
+
const close = src.indexOf(ch, i + 1);
|
|
306
|
+
const stop = close === -1 ? src.length : close + 1;
|
|
307
|
+
atom += src.slice(i, stop);
|
|
308
|
+
i = stop;
|
|
309
|
+
}
|
|
310
|
+
else if (src.startsWith("&&", i) || src.startsWith("||", i)) {
|
|
311
|
+
flush();
|
|
312
|
+
tokens.push(src.slice(i, i + 2));
|
|
313
|
+
i += 2;
|
|
314
|
+
}
|
|
315
|
+
else if (ch === "(" || ch === ")") {
|
|
316
|
+
flush();
|
|
317
|
+
tokens.push(ch);
|
|
318
|
+
i += 1;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
atom += ch;
|
|
322
|
+
i += 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
flush();
|
|
326
|
+
return tokens;
|
|
327
|
+
}
|
|
328
|
+
/** Whether the expression still has a path that could have been evaluated, given
|
|
329
|
+
* the variables the response actually returned a column for.
|
|
330
|
+
*
|
|
331
|
+
* The boolean structure decides, not the mere presence of a `||`:
|
|
332
|
+
* - a term is evaluable when every variable it names has a column;
|
|
333
|
+
* - `A && B` needs BOTH sides — a missing mandatory conjunct is an unprovable
|
|
334
|
+
* claim even if the other side is a disjunction;
|
|
335
|
+
* - `A || B` needs only one — a row can legitimately match through one operand
|
|
336
|
+
* while the other is not evaluable at all (probed 2026-08-03: `F1 > 0 || F2
|
|
337
|
+
* > 0` over 09992.HK, where `finc_pe_ttm` has no HK coverage, matches on F2).
|
|
338
|
+
*
|
|
339
|
+
* So `F1 && (F2 || F3)` missing F1, and `F1 || F2` missing both, are both
|
|
340
|
+
* unevaluable and must fail — checking only for a `||` anywhere would let them
|
|
341
|
+
* through. */
|
|
342
|
+
export function screenerExpressionIsEvaluable(expression, present) {
|
|
343
|
+
const tokens = tokenizeScreenerExpression(expression ?? "");
|
|
344
|
+
let pos = 0;
|
|
345
|
+
const unit = () => {
|
|
346
|
+
if (tokens[pos] === "(") {
|
|
347
|
+
pos += 1;
|
|
348
|
+
const value = or();
|
|
349
|
+
if (tokens[pos] === ")")
|
|
350
|
+
pos += 1;
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
const atom = tokens[pos++] ?? "";
|
|
354
|
+
// A term naming no variable (a literal comparison) is always evaluable.
|
|
355
|
+
return (atom.replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? []).every((ref) => present.has(ref));
|
|
356
|
+
};
|
|
357
|
+
const and = () => {
|
|
358
|
+
let value = unit();
|
|
359
|
+
// Evaluate both sides before combining: short-circuiting would leave the
|
|
360
|
+
// parser mid-expression.
|
|
361
|
+
while (tokens[pos] === "&&") {
|
|
362
|
+
pos += 1;
|
|
363
|
+
const right = unit();
|
|
364
|
+
value = value && right;
|
|
365
|
+
}
|
|
366
|
+
return value;
|
|
367
|
+
};
|
|
368
|
+
const or = () => {
|
|
369
|
+
let value = and();
|
|
370
|
+
while (tokens[pos] === "||") {
|
|
371
|
+
pos += 1;
|
|
372
|
+
const right = and();
|
|
373
|
+
value = value || right;
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
};
|
|
377
|
+
return or();
|
|
378
|
+
}
|
|
379
|
+
/** Variables the expression actually filters on, string literals stripped. These
|
|
380
|
+
* are the bindings whose VALUES the result depends on — a column missing for one
|
|
381
|
+
* of them means the filter cannot be shown to have been applied. */
|
|
382
|
+
export function screenerExpressionFields(expression) {
|
|
383
|
+
return (expression ?? "").replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? [];
|
|
384
|
+
}
|
|
291
385
|
// Parse the screener's `--indicator "F1:code"` bindings and merge each one's
|
|
292
386
|
// `--indicator-param "F1:key=value"` specs. Params key off the variable, not the
|
|
293
387
|
// code, because the same indicator may legitimately appear under two variables
|
|
@@ -320,26 +414,10 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
|
320
414
|
// The server does reject this (100003), but only after a round trip — and the
|
|
321
415
|
// symmetric mistake (binding a variable the expression never uses) it accepts
|
|
322
416
|
// silently while still billing the extra column.
|
|
323
|
-
for (const ref of (expression
|
|
417
|
+
for (const ref of screenerExpressionFields(expression)) {
|
|
324
418
|
if (!indicators.has(ref)) {
|
|
325
419
|
throw new ValidationError(`--expression references "${ref}", which no --indicator binds`);
|
|
326
420
|
}
|
|
327
421
|
}
|
|
328
422
|
return [...indicators.values()];
|
|
329
423
|
}
|
|
330
|
-
/** Indicator codes bound to more than one screener variable, in first-seen
|
|
331
|
-
* order. The API is specified to allow this (one code under two variables with
|
|
332
|
-
* different parameters), but the server currently answers such a request with an
|
|
333
|
-
* empty result set — probed 2026-08-02. Callers warn rather than reject: the
|
|
334
|
-
* capability is intended and a server-side fix is in flight, so a hard block
|
|
335
|
-
* here would have to be reverted. */
|
|
336
|
-
export function duplicateScreenerCodes(indicators) {
|
|
337
|
-
const seen = new Set();
|
|
338
|
-
const duplicated = new Set();
|
|
339
|
-
for (const { indicatorCode } of indicators) {
|
|
340
|
-
if (seen.has(indicatorCode))
|
|
341
|
-
duplicated.add(indicatorCode);
|
|
342
|
-
seen.add(indicatorCode);
|
|
343
|
-
}
|
|
344
|
-
return [...duplicated];
|
|
345
|
-
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -427,8 +427,9 @@ export class GangtiseClient {
|
|
|
427
427
|
},
|
|
428
428
|
}).catch((error) => {
|
|
429
429
|
// Until 2026-08-01 EDE answered a no-data query with 999999, so this hint
|
|
430
|
-
// led with "多为查询无数据". No-data now comes back as
|
|
431
|
-
//
|
|
430
|
+
// led with "多为查询无数据". No-data now comes back as a null CELL with its
|
|
431
|
+
// row and column intact (re-probed 2026-08-08), which leaves 999999 meaning
|
|
432
|
+
// an actual server fault — but the parameter checklist
|
|
432
433
|
// is still the right first move, because a wrong param name or date axis
|
|
433
434
|
// silently yields an EMPTY TABLE rather than this error. Only the data
|
|
434
435
|
// endpoints take a date/security/params; indicator.search shares the
|
|
@@ -436,7 +437,7 @@ export class GangtiseClient {
|
|
|
436
437
|
// instead of nonsensical date/scope/param guidance.
|
|
437
438
|
const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
|
|
438
439
|
if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
|
|
439
|
-
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList
|
|
440
|
+
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名是静默失效)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——有效 code 无数据现在返回保留行列的 null 单元格,空表则多半是整个轴的 code 都没被识别。');
|
|
440
441
|
}
|
|
441
442
|
throw error;
|
|
442
443
|
});
|
|
@@ -36,12 +36,17 @@ export function buildStockPoolStocksBody(options) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Parameter keys that REPLACE an indicator's `tradeDate`. The 2026-08-01 EDE
|
|
38
38
|
* revision dropped the root-level `date`, so `--date` now fans out into each
|
|
39
|
-
* indicator's `tradeDate`.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
39
|
+
* indicator's `tradeDate`. A caller-supplied `reportDate` suppresses that
|
|
40
|
+
* injection for its indicator, so the two date fields are never sent together.
|
|
41
|
+
*
|
|
42
|
+
* This used to be load-bearing in a stronger sense: a report-period indicator
|
|
43
|
+
* answered a `tradeDate` with an EMPTY result rather than an error (probed
|
|
44
|
+
* 2026-08-01 on `is_op_rev_mom`). Re-probed 2026-08-08 — the server now resolves
|
|
45
|
+
* a `tradeDate` to the enclosing report period, and `is_op_rev_mom` returns the
|
|
46
|
+
* same 33.4903 either way. Kept regardless: sending the caller's own date field
|
|
47
|
+
* rather than a second one alongside it is the honest request, and it survives a
|
|
48
|
+
* rollback. Injecting into an indicator that takes no date at all is harmless —
|
|
49
|
+
* `pty_op_scope` still answers normally with a stray `tradeDate` attached.
|
|
45
50
|
*
|
|
46
51
|
* `sDate` is deliberately NOT here. It is an interval START, not a substitute:
|
|
47
52
|
* `qte_vol_intvl` declares `tradeDate` required (the interval END) and `sDate`
|
|
@@ -92,11 +97,15 @@ export function buildIndicatorScreenerBody(options) {
|
|
|
92
97
|
universe: maybeArray(options.security),
|
|
93
98
|
expression: options.expression,
|
|
94
99
|
// Every indicator gets a date, including ones whose parameterList is empty.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
+
// Harmless there (a parameterless indicator answers normally with a stray
|
|
101
|
+
// tradeDate), and it keeps one rule for the whole list rather than a
|
|
102
|
+
// per-indicator exception.
|
|
103
|
+
//
|
|
104
|
+
// It also used to be load-bearing: through 2026-08-02 the screener DROPPED
|
|
105
|
+
// any indicator sent with `parameters: []`, so the official doc's own
|
|
106
|
+
// `F3 contains '酒'` example could not work as written. Re-probed 2026-08-03:
|
|
107
|
+
// fixed server-side. Kept anyway — unconditional is simpler than conditional,
|
|
108
|
+
// and it survives a rollback.
|
|
100
109
|
indicatorList: indicators.map((indicator) => (indicator.parameters.some((param) => DATE_PARAM_KEYS.has(param.paramKey))
|
|
101
110
|
? indicator
|
|
102
111
|
: { ...indicator, parameters: [...indicator.parameters, { paramKey: "tradeDate", paramValue: options.date }] })),
|
|
@@ -49,6 +49,24 @@ const ENDPOINT_DEFS = {
|
|
|
49
49
|
// 50/篇 — same price tier as the AI Agent calls; billing probed non-idempotent.
|
|
50
50
|
retry: "no-replay",
|
|
51
51
|
},
|
|
52
|
+
"insight.pamirs-summary.list": {
|
|
53
|
+
method: "POST",
|
|
54
|
+
path: "/application/open-insight/pamirs-summary/getList",
|
|
55
|
+
kind: "json",
|
|
56
|
+
description: "List Pamirs expert summaries (requires the expert-summary database)",
|
|
57
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
58
|
+
},
|
|
59
|
+
"insight.pamirs-summary.download": {
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: "/application/open-insight/pamirs-summary/download/file",
|
|
62
|
+
kind: "download",
|
|
63
|
+
description: "Download a Pamirs expert summary file",
|
|
64
|
+
// The 2026-08-07 spec states an entitlement (the expert-summary database) but
|
|
65
|
+
// no per-call price. Treated as non-idempotent anyway, like its
|
|
66
|
+
// `insight.summary.download` sibling: if it does meter, a 5xx replay
|
|
67
|
+
// double-bills, and the only cost of being wrong is losing one retry.
|
|
68
|
+
retry: "no-replay",
|
|
69
|
+
},
|
|
52
70
|
"insight.roadshow.list": {
|
|
53
71
|
method: "POST",
|
|
54
72
|
path: "/application/open-insight/schedule/roadshow/getList",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -72,7 +72,9 @@ const ERROR_HINTS = {
|
|
|
72
72
|
// --report-date 的用户越导越错。
|
|
73
73
|
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
74
74
|
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
75
|
-
|
|
75
|
+
// 超出的是账号数据权限的时间下界,不是单次窗口太宽——`--fiscal-year 2015` 这种
|
|
76
|
+
// 整个区间都在界外的请求,怎么缩窗口都还是这个码(实测 2026-08-08)。
|
|
77
|
+
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|
|
76
78
|
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
77
79
|
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
78
80
|
"130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法(非法 file-type 也归此码)。",
|
|
@@ -91,6 +93,9 @@ const ERROR_HINTS = {
|
|
|
91
93
|
"210001": "换一篇,或改用 list 取正文摘要。",
|
|
92
94
|
"220001": "改用 list 取正文摘要。",
|
|
93
95
|
"230001": "只有自己上传的文件可下载。",
|
|
96
|
+
// 2026-08-07 新增码。私域模块,`vault wechat-*` 就在这个模块下且明确要求
|
|
97
|
+
// 「已绑定并激活群消息助理」,所以本 CLI 够得着,不能当作不可达而不给提示。
|
|
98
|
+
"230002": "微信账号未绑定——群消息类接口要求先在 Gangtise 端绑定并激活群消息助理、且助理已入群。",
|
|
94
99
|
"240001": "换更早的 --period(如 2025q3 → 2025interim)。",
|
|
95
100
|
"240002": "改述后重新提交。",
|
|
96
101
|
"240003": "对照命令 --help 检查取值。",
|
|
@@ -130,8 +135,10 @@ export class ApiError extends CliError {
|
|
|
130
135
|
/** Server-specified Retry-After (ms), set on 429 responses so the transport
|
|
131
136
|
* backoff can honor it instead of the default exponential schedule. */
|
|
132
137
|
retryAfterMs,
|
|
133
|
-
/** Context-specific hint that beats the generic per-code table — e.g.
|
|
134
|
-
*
|
|
138
|
+
/** Context-specific hint that beats the generic per-code table — e.g. the
|
|
139
|
+
* EDE fetch endpoints replace 999999's generic "系统错误,请稍后重试" with a
|
|
140
|
+
* parameter checklist, because a wrong param name or date axis there yields
|
|
141
|
+
* an empty TABLE rather than this code. */
|
|
135
142
|
hintOverride) {
|
|
136
143
|
super(message);
|
|
137
144
|
this.code = code;
|