gangtise-openapi-cli 0.38.0 → 0.40.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 +24 -9
- package/dist/src/cli.js +70 -15
- package/dist/src/core/args.js +4 -0
- package/dist/src/core/asyncContent.js +1 -1
- package/dist/src/core/auth.js +8 -2
- package/dist/src/core/calendarType.js +44 -0
- package/dist/src/core/client.js +18 -11
- package/dist/src/core/download.js +2 -2
- package/dist/src/core/endpoints.js +35 -0
- package/dist/src/core/errors.js +4 -1
- package/dist/src/core/normalize.js +16 -0
- package/dist/src/core/output.js +30 -7
- package/dist/src/core/printer.js +22 -6
- package/dist/src/core/quoteSharding.js +14 -3
- package/dist/src/core/rowSink.js +7 -7
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +10 -6
- package/gangtise-openapi/references/commands/ai.md +6 -5
- package/gangtise-openapi/references/commands/indicator.md +30 -18
- package/gangtise-openapi/references/commands/vault.md +36 -1
- package/gangtise-openapi/references/errors.md +13 -3
- package/gangtise-openapi/references/examples.md +29 -0
- package/gangtise-openapi/references/fields.md +4 -4
- package/gangtise-openapi/references/response-schema.md +5 -0
- package/package.json +1 -1
|
@@ -35,6 +35,22 @@ export function flagMissingFields(data, requested, label) {
|
|
|
35
35
|
rec.missingFields = missing;
|
|
36
36
|
process.stderr.write(`[gangtise] warning: ${label} returned no column for ${missing.join(", ")} — the server drops a field name it does not recognise (or no longer serves) without an error. Check the name against references/fields.md; result marked partial (exit 3).\n`);
|
|
37
37
|
}
|
|
38
|
+
export function flagFailedItems(data, label) {
|
|
39
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
40
|
+
return;
|
|
41
|
+
const rec = data;
|
|
42
|
+
if (!Array.isArray(rec.failList) || rec.failList.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
rec.partial = true;
|
|
45
|
+
const detail = rec.failList.map((item) => {
|
|
46
|
+
if (!item || typeof item !== "object")
|
|
47
|
+
return String(item);
|
|
48
|
+
const entry = item;
|
|
49
|
+
const id = entry.securityCode ?? entry.poolId ?? JSON.stringify(entry);
|
|
50
|
+
return entry.failReason ? `${String(id)}(${String(entry.failReason)})` : String(id);
|
|
51
|
+
}).join("、");
|
|
52
|
+
process.stderr.write(`[gangtise] warning: ${label} — ${rec.failList.length} item(s) failed while the request itself succeeded: ${detail}. Result marked partial (exit 3).\n`);
|
|
53
|
+
}
|
|
38
54
|
export function assertColumnarHeader(fields, source) {
|
|
39
55
|
if (!fields) {
|
|
40
56
|
throw new ValidationError(`响应包含数组形式的行但没有 fieldList,无法确定各列的含义,已拒绝输出。这是上游响应结构异常,请报障${traceSuffix(source)}。`);
|
package/dist/src/core/output.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import { ConfigError } from "./errors.js";
|
|
3
4
|
const OUTPUT_FORMATS = ["table", "json", "jsonl", "csv", "markdown"];
|
|
5
|
+
export function stagingPath(target, suffix = "part") {
|
|
6
|
+
return `${target}.${randomBytes(4).toString("hex")}.${suffix}`;
|
|
7
|
+
}
|
|
8
|
+
export function digestBuffer(content) {
|
|
9
|
+
const buf = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
10
|
+
return { bytes: buf.byteLength, sha256: createHash("sha256").update(buf).digest("hex") };
|
|
11
|
+
}
|
|
12
|
+
export async function digestFile(filePath) {
|
|
13
|
+
const { createReadStream } = await import("node:fs");
|
|
14
|
+
const hash = createHash("sha256");
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
const stream = createReadStream(filePath);
|
|
17
|
+
for await (const chunk of stream) {
|
|
18
|
+
const buf = chunk;
|
|
19
|
+
bytes += buf.byteLength;
|
|
20
|
+
hash.update(buf);
|
|
21
|
+
}
|
|
22
|
+
return { bytes, sha256: hash.digest("hex") };
|
|
23
|
+
}
|
|
4
24
|
export function parseOutputFormat(value) {
|
|
5
25
|
const format = value ?? "table";
|
|
6
26
|
if (OUTPUT_FORMATS.includes(format)) {
|
|
@@ -128,24 +148,24 @@ export function renderOutput(value, format) {
|
|
|
128
148
|
}
|
|
129
149
|
export async function streamOutputToFile(value, format, outputPath) {
|
|
130
150
|
if (format !== "jsonl" && format !== "csv")
|
|
131
|
-
return
|
|
151
|
+
return null;
|
|
132
152
|
const list = pickList(value);
|
|
133
153
|
if (!list)
|
|
134
|
-
return
|
|
154
|
+
return null;
|
|
135
155
|
if (list.length < 1000)
|
|
136
|
-
return
|
|
156
|
+
return null;
|
|
137
157
|
let csvRows = [];
|
|
138
158
|
let csvColumns = [];
|
|
139
159
|
if (format === "csv") {
|
|
140
160
|
csvRows = list.filter((row) => Boolean(row && typeof row === "object" && !Array.isArray(row)));
|
|
141
161
|
if (csvRows.length === 0)
|
|
142
|
-
return
|
|
162
|
+
return null;
|
|
143
163
|
csvColumns = Array.from(new Set(csvRows.flatMap((row) => Object.keys(row))));
|
|
144
164
|
}
|
|
145
165
|
const { dirname } = await import("node:path");
|
|
146
166
|
const { createWriteStream } = await import("node:fs");
|
|
147
167
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
148
|
-
const partPath =
|
|
168
|
+
const partPath = stagingPath(outputPath);
|
|
149
169
|
const stream = createWriteStream(partPath, { encoding: "utf8" });
|
|
150
170
|
stream.on("error", () => { });
|
|
151
171
|
try {
|
|
@@ -164,7 +184,9 @@ export async function streamOutputToFile(value, format, outputPath) {
|
|
|
164
184
|
await new Promise((resolve, reject) => {
|
|
165
185
|
stream.end((err) => err ? reject(err) : resolve());
|
|
166
186
|
});
|
|
187
|
+
const digest = await digestFile(partPath);
|
|
167
188
|
await fs.rename(partPath, outputPath);
|
|
189
|
+
return digest;
|
|
168
190
|
}
|
|
169
191
|
catch (error) {
|
|
170
192
|
stream.destroy();
|
|
@@ -174,7 +196,6 @@ export async function streamOutputToFile(value, format, outputPath) {
|
|
|
174
196
|
await fs.unlink(partPath).catch(() => { });
|
|
175
197
|
throw error;
|
|
176
198
|
}
|
|
177
|
-
return true;
|
|
178
199
|
}
|
|
179
200
|
export function pickList(value) {
|
|
180
201
|
if (Array.isArray(value))
|
|
@@ -213,7 +234,7 @@ export async function saveOutputIfNeeded(content, outputPath) {
|
|
|
213
234
|
}
|
|
214
235
|
const { dirname } = await import("node:path");
|
|
215
236
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
216
|
-
const partPath =
|
|
237
|
+
const partPath = stagingPath(outputPath);
|
|
217
238
|
try {
|
|
218
239
|
if (typeof content === "string") {
|
|
219
240
|
await fs.writeFile(partPath, content, "utf8");
|
|
@@ -221,7 +242,9 @@ export async function saveOutputIfNeeded(content, outputPath) {
|
|
|
221
242
|
else {
|
|
222
243
|
await fs.writeFile(partPath, content);
|
|
223
244
|
}
|
|
245
|
+
const digest = digestBuffer(content);
|
|
224
246
|
await fs.rename(partPath, outputPath);
|
|
247
|
+
return digest;
|
|
225
248
|
}
|
|
226
249
|
catch (error) {
|
|
227
250
|
await fs.unlink(partPath).catch(() => { });
|
package/dist/src/core/printer.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { normalizeRows } from "./normalize.js";
|
|
4
|
-
import { countOutputRows, pickList, renderOutput, saveOutputIfNeeded, streamOutputToFile } from "./output.js";
|
|
4
|
+
import { countOutputRows, digestFile, pickList, renderOutput, saveOutputIfNeeded, stagingPath, streamOutputToFile } from "./output.js";
|
|
5
5
|
import { getRowSink } from "./rowSink.js";
|
|
6
6
|
import { extractTitles, writeTitleCache } from "./titleCache.js";
|
|
7
7
|
import { CLI_VERSION } from "../version.js";
|
|
8
8
|
const LARGE_RESULT_ROWS = 50_000;
|
|
9
|
+
export const SUPERSEDED_EXIT = 4;
|
|
10
|
+
export async function warnIfSuperseded(output, digest) {
|
|
11
|
+
const actual = await digestFile(output).catch(() => undefined);
|
|
12
|
+
if (!actual || actual.sha256 === digest.sha256)
|
|
13
|
+
return;
|
|
14
|
+
process.stderr.write(`[gangtise] warning: ${output} was replaced by another export to the same path while this one was finishing — the file there is not this command's output `
|
|
15
|
+
+ `(wrote sha256 ${digest.sha256.slice(0, 12)}…, found ${actual.sha256.slice(0, 12)}…). Give concurrent exports distinct --output paths.\n`);
|
|
16
|
+
if (process.exitCode !== 3)
|
|
17
|
+
process.exitCode = SUPERSEDED_EXIT;
|
|
18
|
+
}
|
|
9
19
|
function warnIfLargeInMemory(items, format) {
|
|
10
20
|
if (items && items.length >= LARGE_RESULT_ROWS) {
|
|
11
21
|
process.stderr.write(`[gangtise] note: ${items.length} rows in '${format}' is built entirely in memory; stream large exports to a file with --format jsonl --output <path> (or csv).\n`);
|
|
@@ -62,7 +72,7 @@ async function stageExportMeta(output, format, rows, columns, normalized, comple
|
|
|
62
72
|
result: redactSecrets(result),
|
|
63
73
|
};
|
|
64
74
|
const metaPath = `${output}.meta.json`;
|
|
65
|
-
const partPath =
|
|
75
|
+
const partPath = stagingPath(metaPath);
|
|
66
76
|
await fs.mkdir(path.dirname(output), { recursive: true });
|
|
67
77
|
try {
|
|
68
78
|
await fs.writeFile(partPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
@@ -72,8 +82,10 @@ async function stageExportMeta(output, format, rows, columns, normalized, comple
|
|
|
72
82
|
throw error;
|
|
73
83
|
}
|
|
74
84
|
return {
|
|
75
|
-
async commit() {
|
|
85
|
+
async commit(digest) {
|
|
76
86
|
try {
|
|
87
|
+
if (digest)
|
|
88
|
+
await fs.writeFile(partPath, `${JSON.stringify({ ...doc, ...digest }, null, 2)}\n`, "utf8");
|
|
77
89
|
await fs.rename(partPath, metaPath);
|
|
78
90
|
}
|
|
79
91
|
catch (error) {
|
|
@@ -118,21 +130,25 @@ export async function printData(data, format, output, cache) {
|
|
|
118
130
|
const staged = format === "csv" || format === "jsonl"
|
|
119
131
|
? await stageExportMeta(output, format, streamed ? streamed.dataRows : countOutputRows(normalized, format), columns, normalized, complete)
|
|
120
132
|
: null;
|
|
133
|
+
let digest;
|
|
121
134
|
try {
|
|
122
135
|
if (streamed) {
|
|
123
136
|
await streamed.finish();
|
|
137
|
+
digest = streamed.digest;
|
|
124
138
|
}
|
|
125
|
-
else if (!(await streamOutputToFile(normalized, format, output))) {
|
|
139
|
+
else if (!(digest = (await streamOutputToFile(normalized, format, output)) ?? undefined)) {
|
|
126
140
|
warnIfLargeInMemory(items, format);
|
|
127
141
|
const content = renderOutput(normalized, format);
|
|
128
|
-
await saveOutputIfNeeded(format === "csv" ? `\ufeff${content}` : content, output);
|
|
142
|
+
digest = await saveOutputIfNeeded(format === "csv" ? `\ufeff${content}` : content, output);
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
catch (error) {
|
|
132
146
|
await staged?.discard();
|
|
133
147
|
throw error;
|
|
134
148
|
}
|
|
135
|
-
await staged?.commit();
|
|
149
|
+
await staged?.commit(digest);
|
|
150
|
+
if (digest)
|
|
151
|
+
await warnIfSuperseded(output, digest);
|
|
136
152
|
process.stdout.write(`${output}\n`);
|
|
137
153
|
return;
|
|
138
154
|
}
|
|
@@ -16,8 +16,10 @@ function formatDate(d) {
|
|
|
16
16
|
return d.toISOString().slice(0, 10);
|
|
17
17
|
}
|
|
18
18
|
function columnRemap(header, shard) {
|
|
19
|
+
const headerNames = new Set(header.map(String));
|
|
20
|
+
const extra = shard.map(String).filter((field) => !headerNames.has(field));
|
|
19
21
|
if (header.length === shard.length && header.every((field, i) => field === shard[i]))
|
|
20
|
-
return null;
|
|
22
|
+
return { map: null, extra };
|
|
21
23
|
const index = new Map(shard.map((field, i) => [String(field), i]));
|
|
22
24
|
const map = [];
|
|
23
25
|
for (const field of header) {
|
|
@@ -26,7 +28,7 @@ function columnRemap(header, shard) {
|
|
|
26
28
|
return undefined;
|
|
27
29
|
map.push(i);
|
|
28
30
|
}
|
|
29
|
-
return map;
|
|
31
|
+
return { map, extra };
|
|
30
32
|
}
|
|
31
33
|
export function isFullMarket(body, fullMarketValue) {
|
|
32
34
|
const list = body.securityList;
|
|
@@ -126,6 +128,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
126
128
|
const objectKeys = new Set();
|
|
127
129
|
const truncatedShards = [];
|
|
128
130
|
const partialShards = [];
|
|
131
|
+
const droppedColumns = new Set();
|
|
129
132
|
const keep = async (rows) => {
|
|
130
133
|
count += rows.length;
|
|
131
134
|
if (!fieldList)
|
|
@@ -179,8 +182,11 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
179
182
|
process.stderr.write(`[gangtise] warning: shard ${shards[i].startDate}..${shards[i].endDate} returned columns that cannot be aligned with the first shard's fieldList; its rows were dropped (see failedShards)\n`);
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
185
|
+
for (const field of remap?.extra ?? [])
|
|
186
|
+
droppedColumns.add(field);
|
|
182
187
|
const rows = rec.list;
|
|
183
|
-
|
|
188
|
+
const map = remap?.map;
|
|
189
|
+
await keep(map ? rows.map((item) => (Array.isArray(item) ? map.map((k) => item[k]) : item)) : rows);
|
|
184
190
|
};
|
|
185
191
|
await runInOrder(shards, config.concurrency ?? PAGE_CONCURRENCY, fetchShard, mergeShard);
|
|
186
192
|
if (failedShards.length === shards.length) {
|
|
@@ -211,6 +217,11 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
211
217
|
out.partial = true;
|
|
212
218
|
process.stderr.write(`[gangtise] warning: ${partialShards.length}/${shards.length} shard(s) reported themselves partial; the merged result is marked partial\n`);
|
|
213
219
|
}
|
|
220
|
+
if (droppedColumns.size > 0) {
|
|
221
|
+
out.partial = true;
|
|
222
|
+
out.droppedColumns = [...droppedColumns];
|
|
223
|
+
process.stderr.write(`[gangtise] warning: ${droppedColumns.size} column(s) came back only in later shards and could not be carried (see droppedColumns); the merged fieldList is the first data shard's — re-pull a narrower range to get them.\n`);
|
|
224
|
+
}
|
|
214
225
|
if (truncatedShards.length > 0) {
|
|
215
226
|
out.partial = true;
|
|
216
227
|
out.truncatedShards = truncatedShards;
|
package/dist/src/core/rowSink.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createReadStream, createWriteStream } from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertColumnarHeader, zipFieldRow } from "./normalize.js";
|
|
5
|
-
import { csvEscape, formatScalar, writeLine } from "./output.js";
|
|
5
|
+
import { csvEscape, digestFile, formatScalar, stagingPath, writeLine } from "./output.js";
|
|
6
6
|
import { extractTitles, MAX_TITLES_PER_ENDPOINT } from "./titleCache.js";
|
|
7
7
|
export const ROW_SINK = Symbol("gangtise.rowSink");
|
|
8
8
|
const STREAM_THRESHOLD = 1000;
|
|
@@ -11,6 +11,7 @@ export class ExportSink {
|
|
|
11
11
|
format;
|
|
12
12
|
cache;
|
|
13
13
|
rows = 0;
|
|
14
|
+
digest;
|
|
14
15
|
titles = {};
|
|
15
16
|
titleCount = 0;
|
|
16
17
|
buffer = [];
|
|
@@ -21,10 +22,14 @@ export class ExportSink {
|
|
|
21
22
|
columns = [];
|
|
22
23
|
columnSet = new Set();
|
|
23
24
|
objectRows = 0;
|
|
25
|
+
partPath;
|
|
26
|
+
rowsPath;
|
|
24
27
|
constructor(outputPath, format = "jsonl", cache) {
|
|
25
28
|
this.outputPath = outputPath;
|
|
26
29
|
this.format = format;
|
|
27
30
|
this.cache = cache;
|
|
31
|
+
this.partPath = stagingPath(outputPath);
|
|
32
|
+
this.rowsPath = stagingPath(outputPath, "rows.part");
|
|
28
33
|
}
|
|
29
34
|
get opened() {
|
|
30
35
|
return this.stream !== null;
|
|
@@ -37,12 +42,6 @@ export class ExportSink {
|
|
|
37
42
|
return this.rows;
|
|
38
43
|
return this.objectRows > 0 ? this.objectRows : this.rows;
|
|
39
44
|
}
|
|
40
|
-
get partPath() {
|
|
41
|
-
return `${this.outputPath}.part`;
|
|
42
|
-
}
|
|
43
|
-
get rowsPath() {
|
|
44
|
-
return `${this.outputPath}.rows.part`;
|
|
45
|
-
}
|
|
46
45
|
setFieldList(fields) {
|
|
47
46
|
this.fields = fields;
|
|
48
47
|
}
|
|
@@ -73,6 +72,7 @@ export class ExportSink {
|
|
|
73
72
|
await endStream(stream);
|
|
74
73
|
if (this.format === "csv")
|
|
75
74
|
await this.assembleCsv();
|
|
75
|
+
this.digest = await digestFile(this.partPath);
|
|
76
76
|
await fs.rename(this.partPath, this.outputPath);
|
|
77
77
|
this.finished = true;
|
|
78
78
|
}
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.40.0";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gangtise-openapi
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.40.0"
|
|
4
4
|
description: |-
|
|
5
5
|
通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ description: |-
|
|
|
39
39
|
8. **参数命名差异**:Insight/Quote/Vault 用 `--security`,Fundamental/AI 用 `--security-code`(例外:`ai stock-summary` 用 `--security`,`ai security-clue` 用 `--gts-code`)。
|
|
40
40
|
9. **调试**:`--verbose` 或 `GANGTISE_VERBOSE=1` 打印每个请求的耗时/字节数到 stderr。
|
|
41
41
|
10. **`--field` 字段名必须核对,不确定就别传**(返回全量最稳)。上游对不存在的字段名有两种处理:`quote` 系(realtime / day-kline / minute-kline / fund-flow)名和值一起丢、不报错——CLI 比对请求与返回的列名,缺列标 `partial` + `missingFields` 并退出 3;`fundamental main-business` / `valuation-analysis` 只丢值、字段名照请求回显——CLI 检测到长度不匹配直接报错退出 1(没有 `--field` 的命令如 `alternative edb-data` 报此错则是上游响应结构异常,报障时带上报错末尾的 `(trace …)`)。`--field` 只回点名的列、不自动附带身份列(`fund-flow` 除外):日 K 要一起写进 `securityCode` / `tradeDate`,分钟 K 是 `securityCode` / `tradeTime`,实时行情是 `securityCode`。realtime **无 `close`**(用 `latestPrice`)、**无市值**(总市值走 `indicator cross-section --indicator qte_mkt_cptl`,A/港/美股均有数)。
|
|
42
|
-
11. 🔴 **EDE 取不到数时返 `null` 占位,行列都保留**(`indicator` 截面 / 时序 / 选股),无告警、退出码 0。**报告期类指标(`is_*` 等)的时序尤其要当心**:按日返回,**只有报告期末那几行是真值**,其余全是 `null`,而时序没有任何参数能只返回报告期末——**别对整列直接求均值 / 求和**(`null` 会被 Excel / pandas / SQL 的聚合跳过但行数不变;`jq` 的 `add/length` 连跳都不跳,要先 `map(select(. != null))`)。截面上日期不落在报告期末时整批返 `null`,`screener` 会筛出空集——**那是日期用错了,不是「没有符合条件的标的」**。所以:报告期类指标的日期一律落**报告期末**(`03-31` / `06-30` / `09-30` / `12-31`),截面用 `--indicator-param "<指标code>:reportDate=YYYY-MM-DD"` 显式给(`screener` 用 `"F1:reportDate=..."
|
|
42
|
+
11. 🔴 **EDE 取不到数时返 `null` 占位,行列都保留**(`indicator` 截面 / 时序 / 选股),无告警、退出码 0。**报告期类指标(`is_*` 等)的时序尤其要当心**:按日返回,**只有报告期末那几行是真值**,其余全是 `null`,而时序没有任何参数能只返回报告期末——**别对整列直接求均值 / 求和**(`null` 会被 Excel / pandas / SQL 的聚合跳过但行数不变;`jq` 的 `add/length` 连跳都不跳,要先 `map(select(. != null))`)。截面上日期不落在报告期末时整批返 `null`,`screener` 会筛出空集——**那是日期用错了,不是「没有符合条件的标的」**。所以:报告期类指标的日期一律落**报告期末**(`03-31` / `06-30` / `09-30` / `12-31`),截面用 `--indicator-param "<指标code>:reportDate=YYYY-MM-DD"` 显式给(`screener` 用 `"F1:reportDate=..."`)。🔴 **时序的日历别手动传错**:报告期末常落在非交易日(2024-03-31 / 06-30 都是周日),`--calendar-type TD` 的日期轴里没有那一列,报告期类指标会整行全 `null` 且退出 0。**不传 `--calendar-type` 时 CLI 会自动选**(用免费的 `indicator search` 读 `parameterList`,去重后每个指标码各查一次:全是交易日类指标才发 `TD`,只要有一个报告期类的就走 `ND`,拿不准也走 `ND`),所以**默认不传是安全的**;显式传了就完全按你给的发。详见 `references/commands/indicator.md`。
|
|
43
43
|
|
|
44
44
|
## 工作流(3 步)
|
|
45
45
|
|
|
@@ -56,6 +56,7 @@ description: |-
|
|
|
56
56
|
- **高积分操作先确认**:任何 50 积分/次及以上、或"按条 × 大批量"(如 `stock-summary` 按代码批量数千只、`opinion` 全量翻页、`concept-info` 500/次)→ 先估总积分告知用户再执行(单价见下「积分计费速查」)
|
|
57
57
|
- 下载**必选**格式未定才问:`independent-opinion --file-type`(必选)、`vault record/my-conference --content-type`(record 三种 original/asr/summary、my-conference 两种 asr/summary);其余 download 有默认(多为 `1`=PDF/原始),用户没提格式就用默认、不必问
|
|
58
58
|
- list→download 用户没指定具体文件 → 展示前 10 条让用户挑
|
|
59
|
+
- 🔴 **改动账号数据的命令**:`vault stock-pool-create` / `stock-pool-rename` / `stock-pool-add-stock` / `stock-pool-remove-stock` / `stock-pool-delete` 写的是用户自己的自选股股票池,是这套 CLI 里仅有的写操作。用户没有明确要求「建池 / 改名 / 加自选 / 删自选 / 删池」时**不要调用**;要调用时先复述「往哪个池、加或删哪几只」再执行。`stock-pool-delete` 会连带清掉池内全部关注关系且不可恢复,CLI 因此要求显式 `--yes`——**这个 `--yes` 必须是用户点头之后才加,看到「加上 --yes」的报错不要自动补上重跑**
|
|
59
60
|
|
|
60
61
|
🟡 **自行判断**:
|
|
61
62
|
- 公司名 → 先速查表,否则 `reference securities-search`
|
|
@@ -80,7 +81,7 @@ description: |-
|
|
|
80
81
|
- 🔴 **极贵**:`alternative concept-info` / `concept-securities` **500/次**
|
|
81
82
|
- ✅ **按篇 / 按条计费的接口,没查到内容就不扣分**(空结果 = 0 积分):各 download(`pamirs-summary` 除外,见 ②)、`ai hot-topic`(50/篇)、`ai stock-summary`(3/条,无看点总结的个股不进返回列表也不计费)。**所以「先小范围试探再放大」是安全的**——先用窄条件确认能查到东西,再扩范围。⚠️ **两类不适用**:① **按次计费的**——`ai knowledge-batch` / `management-discuss-*`、AI Agent 那批 50/次、`alternative concept-info` / `concept-securities` 500/次,不管有没有内容都扣,超时报错也可能已经扣过;② **单价未公布的**——`pamirs-summary`(见下),别据此假定
|
|
82
83
|
- ⚠️ **同参数重复调用不免费**:按次计费的那批无缓存命中豁免(`one-pager` 等生成类重复调用每次扣分,即使秒回缓存内容);**按篇/按条的也一样**——重拉同一批 `hot-topic` 就是按条数再计一次费。生成类与列表结果拿到后自行留存复用,别为「刷新」重调
|
|
83
|
-
- ⚠️
|
|
84
|
+
- ⚠️ **这些端点超时/5xx 不自动重放**(共 19 个:AI Agent 那批 + `ai knowledge-batch` / `management-discuss-*` / `hot-topic`、`alternative concept-info`·`concept-securities`、50/篇 的 `summary`·`foreign-report`·`my-conference` download 与同档处理的 `pamirs-summary` download、`tool file-parse` 提交,以及**不计分**的 `vault stock-pool-create`)。仅连接失败、429 与 token 自愈会重试。**贵档那批的理由是重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的会再扣一次,重发按篇/按条计费的会把**已交付的行**再计一次;两种都亏。**`stock-pool-create` 不涉及积分,是重放会把成功报成失败**——池名不允许与已有池重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`
|
|
84
85
|
|
|
85
86
|
<!-- no-replay-endpoints
|
|
86
87
|
上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
|
|
@@ -102,8 +103,10 @@ insight.pamirs-summary.download
|
|
|
102
103
|
insight.summary.download
|
|
103
104
|
tool.file-parse.submit
|
|
104
105
|
vault.my-conference.download
|
|
106
|
+
vault.stock-pool.create
|
|
105
107
|
-->
|
|
106
108
|
- **按单元格**:`indicator cross-section` / `time-series` / `screener`(A股 0.05 / 港股 0.1 / 美股 0.2 积分每 100 单元格;screener 按**筛选前**范围计费,见 `indicator.md`);`ai knowledge-resource-download` 按下游资源计费
|
|
109
|
+
- 🔴 截面 / 时序另有**单次 30000 单元格硬上限**(截面 = 证券数 × 指标数,时序 = 序列数 × 日期数),超出报 `100006`、不返回部分结果——按这个乘积拆批。板块 ID 会展开成全部成分股,实际证券数常远超写进命令的条数
|
|
107
110
|
- **单价未公布**:`insight pamirs-summary list` / `download`——spec 只写了「需购买专家纪要数据库」这个准入门槛,没给单次价格。**别据此假定免费**;大批量拉取前先小量试,或向平台确认
|
|
108
111
|
|
|
109
112
|
### 数据范围(能查多久)
|
|
@@ -162,7 +165,7 @@ vault.my-conference.download
|
|
|
162
165
|
| 跨类型语义搜索(研报+纪要+...) | `ai knowledge-batch`(多个 `--resource-type`) |
|
|
163
166
|
| 知识库原文下载(搜到后取全文) | `ai knowledge-resource-download`(前置:`knowledge-batch` 拿 `resourceType`+`sourceId`;`250001`/旧 `433007`=组合不匹配) |
|
|
164
167
|
| 一页通 / 投资逻辑 / 同业对比 / 调研提纲 | `ai one-pager / investment-logic / peer-comparison / research-outline` |
|
|
165
|
-
| 个股看点 / 投研总结 / 公司速览 | `ai stock-summary`(`--security` **只收具体代码**,单次最多
|
|
168
|
+
| 个股看点 / 投研总结 / 公司速览 | `ai stock-summary`(`--security` **只收具体代码**,单次最多 6000 个、超过时 CLI 报错并提示分批;仅 A 股/港股,不支持 `aShares`/`hkStocks` 全市场批量) |
|
|
166
169
|
| 业绩点评(异步) | `ai earnings-review` |
|
|
167
170
|
| 观点 PK / 多空辩论(异步) | `ai viewpoint-debate` |
|
|
168
171
|
| 投研线索 | `ai security-clue`(前置:`reference securities-search` 拿 `gts-code`) |
|
|
@@ -187,7 +190,8 @@ vault.my-conference.download
|
|
|
187
190
|
| 录音速记 | `vault record-list / record-download` |
|
|
188
191
|
| 我的会议(业绩会/策略会/路演内部记录) | `vault my-conference-list / my-conference-download` |
|
|
189
192
|
| 微信群消息 | `vault wechat-message-list`(先 `vault wechat-chatroom-list` 拿群 ID) |
|
|
190
|
-
|
|
|
193
|
+
| 自选股股票池(查询) | `vault stock-pool-list / stock-pool-stocks` |
|
|
194
|
+
| 自选股股票池(增删改,会改动账号数据) | `vault stock-pool-create / stock-pool-rename / stock-pool-add-stock / stock-pool-remove-stock / stock-pool-delete`(删池须加 `--yes`;详见 `references/commands/vault.md`) |
|
|
191
195
|
| 行业指标搜索(EDB) | `alternative edb-search` |
|
|
192
196
|
| 行业指标时序数据(EDB) | `alternative edb-data` |
|
|
193
197
|
| 题材画像 / 投资逻辑 / 行业空间 / 竞争格局 / 催化事件 | `alternative concept-info`(前置:`reference concept-search` 拿 `concept-id`) |
|
|
@@ -296,7 +300,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
296
300
|
|
|
297
301
|
## 异常处理
|
|
298
302
|
|
|
299
|
-
**退出码**:`0` 完整成功(含合法空结果)/ `3` 有数据但不完整(`partial: true`;stderr 有 warning,`--format json` 才看得见标记,table/csv/jsonl 只有数据行、看不出问题——csv/jsonl 落盘时看旁边 `<file>.meta.json` 的 `complete` 与 `result
|
|
303
|
+
**退出码**:`0` 完整成功(含合法空结果)/ `3` 有数据但不完整(`partial: true`;stderr 有 warning,`--format json` 才看得见标记,table/csv/jsonl 只有数据行、看不出问题——csv/jsonl 落盘时看旁边 `<file>.meta.json` 的 `complete` 与 `result`;该文件还带 `bytes` / `sha256`,用 `shasum -a 256 <file>` 核一下就能确认这份元信息描述的是旁边这份数据。定位字段:页失败 `failedPages`、分片失败 `failedShards`、分片撞行数上限 `truncatedShards`、只有部分分片返回而合并时放不下的列 `droppedColumns`(缩小日期区间单独拉)、`total` 撞服务端上限 `totalCapped`、`--field` 请求了但没回的列 `missingFields`、逐只请求里撞行数上限的证券 `truncatedSecurities`、EDE 整轴没回 `omittedIndicators` / `omittedSecurities`)/ `4` 数据本身写全了,但 `--output` 那个文件在收尾期间被另一次写向同一路径的导出替换(并发跑同一路径才会出现,给它们各自的 `--output` 即可;与 `3` 同时发生时退出 `3`)/ `1` 硬失败。**拿到 3 就必须告知用户缺了哪段,不能当成功静默继续。** 报错行带 `[trace <id>]`,**报障给 Gangtise 时务必带上**。
|
|
300
304
|
|
|
301
305
|
最高频的几个码(全表、「不报错的坑」、`screener` 缺列判据与困境自救见 `references/errors.md`):
|
|
302
306
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
gangtise ai knowledge-batch --query <text> [--query <text2>] [--top <n>] [--resource-type <n>] [--knowledge-name <name>] [--start-time <ts|datetime>] [--end-time <ts|datetime>]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
- `--query`(**必选**,可重复,最多 5
|
|
15
|
+
- `--query`(**必选**,可重复,最多 5 个):缺失时本地报错,不发空请求。**每个 `--query` 整段送出**——句子里的逗号是标点,不会被当成分隔符拆成两个查询;要问多个问题就重复传 `--query`
|
|
16
16
|
- `--top` 默认 10,最大 20
|
|
17
17
|
- `--resource-type`:`10` 券商研报 | `11` 外资研报 | `20` 内部报告 | `40` 首席观点 | `50` 公司公告 | `51` 港股公告 | `60` 会议平台纪要 | `70` 调研纪要公告 | `80` 网络资源纪要 | `90` 产业公众号
|
|
18
18
|
- `--knowledge-name`:`system_knowledge_doc` 系统知识库 | `tenant_knowledge_doc` 机构知识库
|
|
@@ -55,12 +55,11 @@ gangtise ai peer-comparison --security-code <code>
|
|
|
55
55
|
gangtise ai stock-summary --security <code> [--security <code2> ...]
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
- `--security`(**必选**,可重复):**只收具体证券代码**,单次最多 5000 个(CLI 本地拦截,超过报错并提示分批)
|
|
58
|
+
- `--security`(**必选**,可重复):**只收具体证券代码**,单次最多 6000 个(接口上限,CLI 同值拦截,超过报错并提示分批)——全 A 股可一次提交完
|
|
61
59
|
- 🔴 **不支持全市场批量**:`aShares` / `hkStocks` 这类市场关键词不被接受,CLI 会本地报错(服务端对它们返 `120001`,读起来像代码写错,容易误判)。需要覆盖一批标的就先用 `reference securities-search` 或自有清单取到代码,再分批传入
|
|
62
60
|
- **仅支持 A 股和港股**
|
|
63
|
-
- **积分**:`3
|
|
61
|
+
- **积分**:`3`/条;个股若无看点总结则不在返回列表中,也不扣分——所以**返回行数少于提交数是正常的**,不是丢了数据
|
|
62
|
+
- 🔴 **整批返 0 行别直接读成「这批都没有看点」**:它与「确实都没有看点」长得一样。挑其中一两只单查确认后再下结论
|
|
64
63
|
- 返回字段:`securityCode` / `securityName` / `summary`(精炼投研总结)/ `date`(更新日期 `yyyy-MM-dd`)
|
|
65
64
|
|
|
66
65
|
**示例:**
|
|
@@ -87,6 +86,8 @@ gangtise ai earnings-review-check --data-id <id>
|
|
|
87
86
|
- `--period`:`年份+报告期`,如 `2025q3`(q1/interim/q3/annual),仅 A 股,覆盖最近 6 期
|
|
88
87
|
- `--wait`(**推荐**):阻塞等待到出结果(最长约 5 分钟:14 次指数退避轮询 5s→30s,累计 ≈316s)——**用它时把工具/命令超时设到 ≥360s**,否则外层先超时
|
|
89
88
|
- 不带 `--wait` 的手动轮询:① `earnings-review` → 拿 `{dataId, status, hint}` → ② 间隔 ~30s `*-check`(预算 ~2-3 分钟)→ pending 继续 → 多次仍 pending 交用户稍后手动 check
|
|
89
|
+
- 这一步的 `{dataId, status, hint}` 同样受 `--format` / `--output` 控制,**加了 `--output` 就会落盘**,脚本可以直接从文件里取 `dataId` 再去轮询
|
|
90
|
+
- `*-check` 返回「还在生成中」时同样落盘(`{dataId, status: "pending", hint}`),所以轮询脚本每一轮都能从同一个文件读状态,不必区分「出结果了」和「还没好」两种取法
|
|
90
91
|
- 错误码:`140001`(旧 `410110`)生成中,继续等待;`140002`(旧 `410111`)生成失败,终态不重试。CLI 两代码都识别
|
|
91
92
|
|
|
92
93
|
## 观点 PK `ai viewpoint-debate`(异步)
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
| A股盈利预测 / 一致预期(含预测 EPS) | `fundamental earning-forecast`;EDE 搜到的基本 / 稀释 EPS 是已实现值,不能替代预测 |
|
|
15
15
|
| A股估值历史分位 | `fundamental valuation-analysis` |
|
|
16
16
|
| **估值指标的历史序列**(分位 / 回测 / 择时) | 两个接口口径不同、都要交叉核——EDE 按正式财报披露日切换财报口径,`valuation-analysis` 按业绩快报切,见下方专门小节 |
|
|
17
|
-
| 开高低收 / 成交量等行情与 K 线 | `quote
|
|
17
|
+
| 开高低收 / 成交量等行情与 K 线 | `quote`;免费且支持多证券批量。**换接口不换口径**:`quote` 的 `volume` 与 EDE `qte_vol` 逐位相同(A股 / 港股 / 美股,单位均为股)。EDE 只在 `quote` 不提供的口径上才需要——周 / 月 / 季 / 年 / 区间成交量 |
|
|
18
18
|
| 单证券资金流向(要占比字段) | `quote fund-flow`;免费,且有 EDE 没有的 `*Ratio` 占比。多证券批量取某一两个档位才用 EDE 的 `flow_*` |
|
|
19
19
|
| 单证券三大报表全部科目 | 对应 `fundamental` 利润表 / 资产负债表 / 现金流量表命令 |
|
|
20
20
|
|
|
21
|
-
EDE 不是“搜到就优先”。取数前必须核对:① `indicatorName` + `description` 与目标语义一致;② `scopeList` 覆盖**全部**目标市场和证券类型;③ `parameterList` 的必填参数与枚举可满足。`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退上表的专用接口;专用接口也不支持目标市场时,如实说明当前 CLI 无可用口径,不能用其他语义代替。`valuation-analysis` / `earning-forecast` 仅支持 A 股,港 / 美股的**估值历史分位**与**盈利预测**当前无可用口径。但**估值指标本身别照抄旧结论**:`finc_pe_ttm`(PE TTM)
|
|
21
|
+
EDE 不是“搜到就优先”。取数前必须核对:① `indicatorName` + `description` 与目标语义一致;② `scopeList` 覆盖**全部**目标市场和证券类型;③ `parameterList` 的必填参数与枚举可满足。`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退上表的专用接口;专用接口也不支持目标市场时,如实说明当前 CLI 无可用口径,不能用其他语义代替。`valuation-analysis` / `earning-forecast` 仅支持 A 股,港 / 美股的**估值历史分位**与**盈利预测**当前无可用口径。但**估值指标本身别照抄旧结论**:`finc_pe_ttm`(PE TTM) 与 `finc_pb_mrq`(PB MRQ) 港股都有数。⚠️ **本文里所有「仅 A 股」「无数据」这类否定结论都只是某个时点的抽查**——服务端在持续补数据,正面结论过期会给错数,负面结论过期则会让你白白拒掉一个现在能跑的查询(不报错、不告警,只是少调一次)。**一律以当次 `scopeList` + 抽查一行为准。**`search` 免费,EDE 取数按单元格计费;除多证券批量的效率收益外,仍优先免费 / 低价的 `quote` 或 `fundamental`。
|
|
22
22
|
|
|
23
23
|
## 指标搜索 `indicator search`
|
|
24
24
|
|
|
@@ -30,9 +30,9 @@ gangtise indicator search --keyword <text> [--limit <n>]
|
|
|
30
30
|
- `--limit`:返回条数上限,默认 50,最大 100
|
|
31
31
|
- 默认 `--format table` 只适合浏览名称;正式路由 / 取数前必须加 `--format json`,才能完成语义、`scopeList`、`parameterList` 三项校验
|
|
32
32
|
- 返回字段:`indicatorCode` / `indicatorName` / `description`(算法与口径)/ `scopeList`(该指标适用的市场 + 证券类型 + `usageRestriction`)/ `parameterList`(可传的 `--indicator-param` 参数名及枚举)/ `score`
|
|
33
|
-
- **市场范围按指标判断**:`scopeList` 会返回实际覆盖范围,且指标之间不同;不能笼统写成每个指标都覆盖 A / 港 / 美股。覆盖面在持续扩,别把抽查结论当常量:例如 `qte_mkt_cptl`/`shr_tot` 覆盖 A/港/美股,`finc_pe_ttm`
|
|
33
|
+
- **市场范围按指标判断**:`scopeList` 会返回实际覆盖范围,且指标之间不同;不能笼统写成每个指标都覆盖 A / 港 / 美股。覆盖面在持续扩,别把抽查结论当常量:例如 `qte_mkt_cptl`/`shr_tot` 覆盖 A/港/美股,`finc_pe_ttm` / `finc_pb_mrq` 覆盖 A/港股,`is_op_rev_ttm`/`is_shnp_ttm` 覆盖 A/港股。目标列表含任一 scope 外证券时,本批 EDE 校验不通过,应回退专用接口
|
|
34
34
|
- **`scopeList[].usageRestriction`**:该指标在具体接口上的限制,`null` = 无限制。⚠️ **它是提示不是硬约束**——标着「不支持指标时间序列接口」的 `qte_vol_intvl` 调时序照样返数据、不会被拦下。当成「口径可能不对、结果别当真」看,别指望它会报错
|
|
35
|
-
- ⚠️ **`scopeList`
|
|
35
|
+
- ⚠️ **`scopeList` 是声明不是保证**:它可能**超前于数据**(声明覆盖但该市场暂无数据),也可能**滞后于数据**(已有数据尚未声明)。**「某某仍无数据」这类否定结论过期得和正面结论一样快**,照着它走会白白拒掉一个现在能跑的查询,而且不报错。**判断成本很低**:无数据会保留行列、给一个占位单元格,抽查一行就能看出覆盖情况;code 写错则直接报 `100003`,两种情形不会混淆。占位值统一是 `null`(详见下方「缺数据 vs 代码写错」)
|
|
36
36
|
- 美股代码用交易所后缀 `.O`(NASDAQ) / `.N`(NYSE):用 `AAPL.O`,`AAPL.US` 取不到数据
|
|
37
37
|
|
|
38
38
|
```bash
|
|
@@ -94,7 +94,21 @@ gangtise indicator time-series --indicator <code> [--indicator <code2>] \
|
|
|
94
94
|
- `--indicator` / `--security`:同上,但**只允许「多指标 × 单证券」或「单指标 × 多证券」**,不能两边都多个(要多 × 多用 `cross-section`,否则报 `100003`「仅支持多指标单证券或单指标多证券」)。**单指标时 `--security` 才能传板块 ID**;多指标时只能传一个证券代码、不得传板块 ID
|
|
95
95
|
- `--start-date` / `--end-date`(**均必选**):区间端点 `yyyy-MM-dd`。时序的时间范围由这两个参数统管,**不要**再用 `--indicator-param` 传 `tradeDate` 这类单日期参数
|
|
96
96
|
- ⚠️ **同一 `indicatorCode` 挂多套参数:截面和时序都不支持,这是设计如此,要拆成两次调用**。只有 `screener` 支持(它把指标绑到不同变量 `F1`/`F2` 上,天然可区分)。CLI 也表达不了该请求——`--indicator-param` 按 code 建 Map,同 code 的多组参数会被合并成一组,与服务端设计一致。走 `raw call` 真发出去会报 `100003「指标 xxx 重复配置」`
|
|
97
|
-
- `--calendar-type`:日期类型 `ND`(自然日)/`TD`(
|
|
97
|
+
- `--calendar-type`:日期类型 `ND`(自然日)/`TD`(交易日)/`WD`(工作日)。`TD` 且跨市场时,`date` 列是各市场交易日的**并集**。服务端默认 `ND`
|
|
98
|
+
|
|
99
|
+
**不传时 CLI 会自动选**(v0.40.0 起):发请求前用 `indicator search`(免费)读每个指标的 `parameterList`——**去重后每个指标码各查一次**(并发,`--indicator` 只给一个时就是一次)——
|
|
100
|
+
|
|
101
|
+
| 本次请求的指标 | CLI 发什么 | 为什么 |
|
|
102
|
+
| :-- | :-- | :-- |
|
|
103
|
+
| **全部**都声明 `tradeDate`、且**都不**声明 `reportDate` / `fiscalYear` | `TD`,并在 stderr 说明一句 | 行情 / 日频估值序列,交易日轴上一格不少,还省掉周末空行 |
|
|
104
|
+
| 任一指标声明 `reportDate` 或 `fiscalYear` | 不发(服务端按 `ND`) | 见下面那条红字 |
|
|
105
|
+
| `parameterList` 为空、查不到该 code、或这次查询失败 | 不发(服务端按 `ND`) | 拿不准就退回服务端默认,不赌 |
|
|
106
|
+
|
|
107
|
+
**显式传了 `--calendar-type` 就完全按你给的发**,不查也不改。
|
|
108
|
+
|
|
109
|
+
- 🔴 **两个方向的代价不对称,所以自动选择只往 `ND` 一边兜底**:报告期末常落在非交易日(如 2024-03-31、2024-06-30 均为周日),`TD` 的日期轴里没有那一列,报告期类指标于是**整行全 `null`、退出码 0、不报错**,读起来像「这只票没有这项财务数据」。**选 `ND` 选错只是多花单元格,选 `TD` 选错是静默丢数。**
|
|
110
|
+
- **混合两类指标时一律 `ND`**:一次请求只有一个 `calendarType`,管不到单个指标——`ND` 下两类都取得到(行情列在周末是 `null`,本就该是),`TD` 下行情列正常而报告期列整行全空。自动选择按的就是这条:只要有一个报告期类指标,整条走 `ND`
|
|
111
|
+
- **`TD` 能省多少**:A 股一年约 240~245 个交易日,所以同一区间 `ND` 的日期列大约是 `TD` 的 **1.5 倍**,多出来的那三分之一全是非交易日的 `null`。换算到 30000 上限就是「单指标拉一年,`TD` 能带的证券数比 `ND` 多约一半」。**要精确值就用自己的区间各跑一次比 `dates` 长度**,别套这里的比例——交易日数逐年不同
|
|
98
112
|
- `--currency` / `--scale`:同 `cross-section`(含根级 `--scale` 的污染坑)
|
|
99
113
|
- **输出(宽表)**:每行一个日期,列为 `date / <各序列名>…`;序列在「单指标」时是各**证券**,在「单证券多指标」时是各**指标**。**板块 ID 算多证券**——传 1 个 `sectorId` 服务端会展开成 N 只成分股,列就是这 N 只(如中信白酒 → 19 列)
|
|
100
114
|
- **`--key-by name|code`**(默认 `name`):同 `cross-section`;`code` 模式下单证券列=各 `indicatorCode`、多证券列=各 `securityCode`,批量按 code 回填用它
|
|
@@ -192,20 +206,18 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
|
192
206
|
|
|
193
207
|
## 必填参数与错误码(取数前必读)
|
|
194
208
|
|
|
195
|
-
###
|
|
209
|
+
### 🔴 单次调用的单元格上限:30000
|
|
196
210
|
|
|
197
|
-
|
|
211
|
+
`cross-section` 与 `time-series` 各有一个**单次返回单元格总数不得超过 30000** 的硬上限,超出直接报 `100006 QUERY_LIMIT_EXCEEDED`(msg 写明限额),不返回部分结果、不截断。口径就是返回矩阵的元素个数:
|
|
198
212
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
⚠️ **不要用「改取前一个交易日」当通用兜底**:它可能在某个个案上碰巧有效,但坏的是哪一天并无规律,而且会丢掉那一整天的数据。
|
|
213
|
+
| 接口 | 单元格数 | 例 |
|
|
214
|
+
| :-- | :-- | :-- |
|
|
215
|
+
| `cross-section` | **证券数 × 指标数** | 5000 只 × 6 指标 = 30000,可以;5001 只 × 6 指标 = 30006,被拒 |
|
|
216
|
+
| `time-series` | **序列数 × 日期数**(序列 = 单指标时的证券数,或单证券时的指标数) | 300 只 × 100 天 = 30000,可以;301 只 × 100 天 = 30100,被拒 |
|
|
205
217
|
|
|
206
|
-
|
|
218
|
+
**撞上限就按这个乘积拆批**:截面拆证券或拆指标,时序拆证券或缩日期区间,分批取回后自己合并。
|
|
207
219
|
|
|
208
|
-
|
|
220
|
+
⚠️ **两处最容易撞**:① `--security` 传板块 ID 时服务端会展开成全部成分股,实际证券数远大于写进命令的条数(全 A 股板块是五千多只,配 6 个指标就到顶);② 时序的日期轴由 `--calendar-type` 决定,`ND` 的日期列约是 `TD` 的 1.5 倍(周末与节假日各占一列),同一个查询在 `ND` 下更容易超限——纯行情序列走 `TD` 就省下这部分(不传时 CLI 会自动判,见上文)。
|
|
209
221
|
|
|
210
222
|
**缺数据 vs 代码写错**:
|
|
211
223
|
|
|
@@ -232,7 +244,7 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
|
232
244
|
|
|
233
245
|
| 查法 | 服务端返回 |
|
|
234
246
|
| :--- | :--- |
|
|
235
|
-
| `--indicator
|
|
247
|
+
| `--indicator mgn_bal --security 00700.HK`(融资融券余额不覆盖港股) | 1 行,`mgn_bal: null`,**退出 0** |
|
|
236
248
|
| `--indicator qte_close --date 2027-01-04`(未来日期) | 1 行,`qte_close: null`,**退出 0** |
|
|
237
249
|
| `--indicator qte_close --indicator not_a_real_code`(有对照物) | `100003 指标 not_a_real_code 不存在` |
|
|
238
250
|
| `--indicator not_a_real_code`(**无对照物**) | `100003 指标 not_a_real_code 不存在` ← 同样报错 |
|
|
@@ -265,7 +277,7 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
|
265
277
|
| `100001` | **缺少必填参数**:如 `universe` 没传 | 补齐 `--indicator`/`--security` |
|
|
266
278
|
| `100003`@400 | 入参/表达式错误:`time-series` 传了「多指标 × 多证券」、`expression` 引用未声明变量、`indicatorParamList` 的 code 不在 `indicatorCodeList` 里 | 按 msg 改;多 × 多改用 `cross-section`。CLI 已在本地拦截「表达式引用未绑定变量」,不会白发一次请求 |
|
|
267
279
|
| `140002`@500 | **终态参数错**:指标必填参数缺失、枚举越界(如「参数 adjustType 的值 99 不在有效范围内 [1,2,3,4]」)、表达式语法错误 | **不重试**(CLI 已把 140002 列为终态码)。读 `search --format json` 的 `parameterList` 改参数名/取值 |
|
|
268
|
-
| `999999` | 系统故障。「无数据」不用此码,所以它基本只剩真故障。⚠️ 别把它和空表混为一谈:无数据是占位单元格(统一 `null`),空表表示整轴 code 未识别或参数名写错 | CLI 对 indicator
|
|
280
|
+
| `999999` | 系统故障。「无数据」不用此码,所以它基本只剩真故障。⚠️ 别把它和空表混为一谈:无数据是占位单元格(统一 `null`),空表表示整轴 code 未识别或参数名写错 | CLI 对 indicator 端点**不重试此码**;确认参数无误仍报错,稍后再试或联系平台支持 |
|
|
269
281
|
| `110003` | **超出账号数据权限的时间范围**。窗口按**账号**配、不按接口配——`cross-section` / `time-series` / `screener` 同界,`quote day-kline` 也在同一条界上 | 把日期移进范围;整段区间都早于下界时缩短窗口无用,**换接口绕不过去**,要更长历史联系客户经理开通 |
|
|
270
282
|
| `130001`(旧 `410004`) | 数据未找到,或**该指标无权限**(内层信封失败会带具体 msg,如"指标无权限";此码被服务端复用) | 检查查询条件与指标权限;换证券/日期仍失败多为无权限,联系管理员开通 |
|
|
271
283
|
|
|
@@ -360,7 +372,7 @@ gangtise indicator cross-section --indicator scr_indu_citic --indicator scr_indu
|
|
|
360
372
|
## 取数最佳实践
|
|
361
373
|
|
|
362
374
|
- **先 search 做三项校验**:看 `indicatorName` + `description` 确认语义和口径,看 `scopeList` 确认覆盖全部目标市场 / 证券类型(并看 `usageRestriction` 有无接口限制),再看 `parameterList` 补齐必填参数(required)并核对专属参数**名称**与枚举(`adjustType`/`scale`/`currency` 等);任一不符就回退专用接口。**参数名一律以 search 返回的 `parameterList` 为准,不要照抄任何示例里的参数名**——参数名会随版本调整,写错会报 `100003` 并指出是哪个指标的哪个参数,按报错改即可。
|
|
363
|
-
- **`scopeList` 声称覆盖 ≠
|
|
375
|
+
- **`scopeList` 声称覆盖 ≠ 每只都有数**:声明可能超前于数据,同一指标下也可能只有部分证券有数,既不能当「全覆盖」也不能当「无覆盖」。**判断成本很低**:无覆盖会留下占位单元格(行列都在,跟同批查了什么无关),抽查一行即可判定,不用再靠对照组反推。跨市场批量取数后仍建议**逐市场抽查一行**,别默认 scopeList 就是事实——反过来也一样,**否定结论同样会过期**。
|
|
364
376
|
- **公司类型决定有没有这个科目**:财务科目分公司类型——银行有「存放同业」、券商有「客户资金存款」、保险有「预收保费」,一般企业没有。某指标对茅台返回 `null`(无此科目),换到对应类型证券(招行/中信/平安)就有数。
|
|
365
377
|
- **日期路由**:
|
|
366
378
|
- 财务报表类(`bs_`/`is_`/`cf_`/`div_`/`shr_`,以及 description 明确按报告期统计的 `finc_`)→ 多数用**报告期末**(Q1 `2026-03-31`、年报 `2025-12-31`,无需是交易日)。🔴 **但 `_ttm` 后缀是整族例外,必填 `tradeDate`**——`is_*_ttm` / `cf_*_ttm` / `div_cash_ttm` / `finc_*_ttm` 一律走交易日。**一眼验证**:在一个**非报告期末的普通交易日**直接 `--date` 取 `is_op_rev_ttm`,照样返真值;反过来给它传 `reportDate` 会报 `100003 不支持参数 reportDate; 缺少必填参数 tradeDate`。同族对照:普通 `is_op_rev` 只传 `--date` 则报「不支持参数 tradeDate; 缺少必填参数 reportDate」——**两者恰好相反**。**所以前缀只是提示,判据仍是上面 `--date` 那条:读 `parameterList` 里必填的是哪个,别按 code 前缀推断**
|
|
@@ -68,7 +68,11 @@ gangtise vault wechat-chatroom-list [--room-name <name>] [--from <n>] [--size <n
|
|
|
68
68
|
- 省略 `--size` 拉全量(接口返回 `total`,CLI 按 total 并发翻页);传 `--size N` 只取前 N 条。单页最大 50
|
|
69
69
|
- 返回字段:`total`(总条数)/ `chatroomName` / `chatroomId`
|
|
70
70
|
|
|
71
|
-
## 自选股股票池 `vault stock-pool
|
|
71
|
+
## 自选股股票池 `vault stock-pool-*`
|
|
72
|
+
|
|
73
|
+
查询两个、增删改五个,全部免费,只操作当前账号本人的数据。
|
|
74
|
+
|
|
75
|
+
### 查询
|
|
72
76
|
|
|
73
77
|
```bash
|
|
74
78
|
gangtise vault stock-pool-list
|
|
@@ -80,3 +84,34 @@ gangtise vault stock-pool-stocks [--pool-id <id>]
|
|
|
80
84
|
- `--pool-id`:股票池 ID,可重复;不传默认 `all`(返回所有池中的非重复证券)
|
|
81
85
|
- 传入 `--pool-id all` 等同于全量查询,最多返回 10000 只
|
|
82
86
|
- 返回字段:`securityCode` / `securityName`
|
|
87
|
+
|
|
88
|
+
### 增删改
|
|
89
|
+
|
|
90
|
+
🔴 **这五个命令会改动账号数据**,是本 CLI 仅有的写操作;`--pool-id` 一律取自 `stock-pool-list`。
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gangtise vault stock-pool-create --name <名称>
|
|
94
|
+
gangtise vault stock-pool-rename --pool-id <id> --name <新名称>
|
|
95
|
+
gangtise vault stock-pool-add-stock --pool-id <id> --security <code> [--security <code>...]
|
|
96
|
+
gangtise vault stock-pool-remove-stock --pool-id <id> --security <code> [--security <code>...]
|
|
97
|
+
gangtise vault stock-pool-delete --pool-id <id> [--pool-id <id>...] --yes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `stock-pool-create`:建池,返回新池的 `poolId` / `poolName`
|
|
101
|
+
- **池名上限 10 个字符**(按字符计,中文算 1 个),超出返回 `230007`。⚠️ 在 Gangtise 终端里建的老池名可以更长,通过本接口建池 / 改名则一律受这个上限约束
|
|
102
|
+
- **池名不能与已有池重复**,重名返回 `230006`;判重是整串精确比较——首尾空格不会被去掉(`" A "` 与 `"A"` 可并存),大小写也不归一(`abc` 与 `ABC` 可并存)
|
|
103
|
+
- 每个账号最多 30 个池,达到上限后返回 `230003`
|
|
104
|
+
- 这条命令超时或 5xx **不会自动重发**(重发一个其实已经建成的请求只会撞重名报错);超时后先 `stock-pool-list` 看池建成没有,再决定重来
|
|
105
|
+
- `stock-pool-rename`:只改名,池内自选股不受影响;新名的长度与重名规则同上(改成该池自己当前的名字算成功)
|
|
106
|
+
- `stock-pool-add-stock` / `stock-pool-remove-stock`:批量加 / 批量移出,`--security` 可重复或逗号分隔;单池上限 10000 只
|
|
107
|
+
- 代码要带市场后缀且**大小写敏感**:`600519.SH` / `00700.HK` / `AAPL.O` 可混在一次请求里;`600519`(缺后缀)、`600519.sh`、`aapl.o`、`700.HK`(港股要 5 位)都会进 `failList` 报「证券代码不存在」,而不是报错退出
|
|
108
|
+
- 首尾空格会被自动去掉,列表内重复的同一代码会去重(成功项只计一次)
|
|
109
|
+
- `stock-pool-delete`:批量删池,**必须显式加 `--yes`**(不加会直接报错退出,不发请求)
|
|
110
|
+
- 删池会同时移除池内全部证券的关注关系,**不可恢复**;个股的投资笔记独立保留,不受影响
|
|
111
|
+
- **`raw call vault.stock-pool.delete` 同样要 `--yes`**——换成 raw 入口不会跳过这道确认
|
|
112
|
+
|
|
113
|
+
**部分失败会被标出来**:`add-stock` / `remove-stock` / `delete` 是逐条处理的——证券代码不存在这类单条失败,服务端仍返回成功信封,把失败明细放进 `failList`(`successList` 里是成功的那些)。CLI 检测到 `failList` 非空时在 stderr 列出失败项与原因、给结果标 `partial`、**退出码 3**;全部成功才是退出码 0。批量脚本按退出码判断即可,不必自己解析 `failList`。**`raw call` 打这三个端点时判定完全相同**,同一份响应不会一个入口退 3、另一个退 0。
|
|
114
|
+
|
|
115
|
+
**幂等**:重复加已在池内的证券、移出本就不在池内的证券、删不存在的 `poolId`,都算成功并计入 `successList`,不会报错。唯一不幂等的是 `stock-pool-create`(见上面的重名规则)。
|
|
116
|
+
|
|
117
|
+
**返回字段**:`create` / `rename` 返回 `poolId` / `poolName`;`add-stock` / `remove-stock` / `delete` 返回 `successList[]` + `failList[]{securityCode 或 poolId, failReason}`。
|