gangtise-openapi-cli 0.41.1 → 0.43.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.
Files changed (51) hide show
  1. package/README.md +45 -39
  2. package/dist/src/cli.js +50 -1161
  3. package/dist/src/commands/ai.js +156 -0
  4. package/dist/src/commands/alternative.js +56 -0
  5. package/dist/src/commands/auth.js +37 -0
  6. package/dist/src/commands/bond.js +76 -0
  7. package/dist/src/commands/fundamental.js +137 -0
  8. package/dist/src/commands/indicator.js +134 -0
  9. package/dist/src/commands/insight.js +338 -0
  10. package/dist/src/commands/quote.js +181 -0
  11. package/dist/src/commands/raw.js +49 -0
  12. package/dist/src/commands/reference.js +58 -0
  13. package/dist/src/commands/shared.js +197 -0
  14. package/dist/src/commands/tool.js +55 -0
  15. package/dist/src/commands/vault.js +203 -0
  16. package/dist/src/core/args.js +13 -9
  17. package/dist/src/core/asyncContent.js +32 -19
  18. package/dist/src/core/auth.js +3 -2
  19. package/dist/src/core/client.js +112 -15
  20. package/dist/src/core/commandBodies.js +2 -1
  21. package/dist/src/core/config.js +16 -3
  22. package/dist/src/core/download.js +9 -5
  23. package/dist/src/core/endpoints.js +106 -1
  24. package/dist/src/core/exitStatus.js +17 -0
  25. package/dist/src/core/fileParse.js +3 -20
  26. package/dist/src/core/opinionDetail.js +39 -0
  27. package/dist/src/core/output.js +55 -12
  28. package/dist/src/core/perSecurity.js +29 -11
  29. package/dist/src/core/printer.js +7 -8
  30. package/dist/src/core/quoteSharding.js +19 -15
  31. package/dist/src/core/rowSink.js +40 -7
  32. package/dist/src/core/titleCache.js +28 -2
  33. package/dist/src/core/transport.js +7 -4
  34. package/dist/src/core/updateCheck.js +51 -0
  35. package/dist/src/version.js +19 -1
  36. package/gangtise-openapi/SKILL.md +50 -49
  37. package/gangtise-openapi/references/commands/ai.md +35 -16
  38. package/gangtise-openapi/references/commands/alternative.md +2 -2
  39. package/gangtise-openapi/references/commands/bond.md +1 -1
  40. package/gangtise-openapi/references/commands/fundamental.md +5 -5
  41. package/gangtise-openapi/references/commands/indicator.md +29 -27
  42. package/gangtise-openapi/references/commands/insight.md +39 -31
  43. package/gangtise-openapi/references/commands/quote.md +19 -19
  44. package/gangtise-openapi/references/commands/reference-and-lookup.md +18 -15
  45. package/gangtise-openapi/references/commands/vault.md +10 -7
  46. package/gangtise-openapi/references/errors.md +27 -25
  47. package/gangtise-openapi/references/examples.md +38 -36
  48. package/gangtise-openapi/references/fields.md +8 -6
  49. package/gangtise-openapi/references/lookup-ids.md +10 -8
  50. package/gangtise-openapi/references/response-schema.md +7 -7
  51. package/package.json +1 -1
@@ -0,0 +1,338 @@
1
+ import { Command } from "commander";
2
+ import { collectList, dateArg, maybeArray, parseChoiceList, parseFrom, parseSize, parseTimestamp13 } from "../core/args.js";
3
+ import { ValidationError } from "../core/errors.js";
4
+ import { fetchOpinionDetails } from "../core/opinionDetail.js";
5
+ import { rowCount } from "../core/rowSink.js";
6
+ import { emit, addDownloadCommand, confirmCostly, multiChoice, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query } from "./shared.js";
7
+ export const insight = new Command("insight").description("Insight APIs");
8
+ const RESEARCH_AREA_CITIC_OR_DIRECTION = "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here";
9
+ const SW_INDUSTRY_ONLY = "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them";
10
+ const opinion = new Command("opinion");
11
+ const summary = new Command("summary");
12
+ const pamirsSummary = new Command("pamirs-summary");
13
+ const roadshow = new Command("roadshow");
14
+ const siteVisit = new Command("site-visit");
15
+ const strategy = new Command("strategy");
16
+ const forum = new Command("forum");
17
+ const performanceCalendar = new Command("performance-calendar");
18
+ const research = new Command("research");
19
+ const foreignReport = new Command("foreign-report");
20
+ const announcement = new Command("announcement");
21
+ const announcementHk = new Command("announcement-hk");
22
+ const announcementUs = new Command("announcement-us");
23
+ const foreignOpinion = new Command("foreign-opinion");
24
+ const independentOpinion = new Command("independent-opinion");
25
+ const officialAccount = new Command("official-account");
26
+ const qa = new Command("qa");
27
+ const reportImage = new Command("report-image");
28
+ query(opinion, "list", {
29
+ endpoint: (options) => options.withContent ? "insight.opinion.list-with-content" : "insight.opinion.list",
30
+ fields: [
31
+ rankType(),
32
+ list("--research-area <id>", RESEARCH_AREA_CITIC_OR_DIRECTION, "researchAreaList"),
33
+ list("--chief <id>", "Chief ID", "chiefList"),
34
+ list("--security <code>", "Security code", "securityList"),
35
+ list("--broker <id>", "Broker ID", "brokerList"),
36
+ list("--industry <id>", "Industry ID", "industryList"),
37
+ list("--concept <id>", "Concept ID", "conceptList"),
38
+ list("--llm-tag <tag>", "Semantic tag", "llmTagList"),
39
+ list("--source <source>", "Source", "sourceList"),
40
+ flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) instead of the 200-char brief (1 credit/row). v1 shape: title and body sit under contentList.title / contentList.content, and there is no brief"),
41
+ format(), output("Output path"),
42
+ ...timeFilters(),
43
+ ],
44
+ });
45
+ query(summary, "list", {
46
+ endpoint: "insight.summary.list",
47
+ cache: { endpointKey: "insight.summary.list", idField: "summaryId" },
48
+ fields: [
49
+ searchType(), rankType(),
50
+ numberList("--source <number>", "Source type", "sourceList"),
51
+ list("--research-area <id>", "Research area ID; this endpoint accepts all three code sets: citicIndustry (1008001xx), swIndustry (104xx0000), gangtiseIndustry direction (122000xxx)", "researchAreaList"),
52
+ list("--security <code>", "Security code", "securityList"),
53
+ list("--institution <id>", "Institution ID", "institutionList"),
54
+ list("--category <name>", "Category", "categoryList"),
55
+ list("--market <name>", "Market", "marketList"),
56
+ list("--participant-role <name>", "Participant role", "participantRoleList"),
57
+ format(), output("Output path"),
58
+ ...timeFilters(),
59
+ ],
60
+ });
61
+ addDownloadCommand(summary, { endpointKey: "insight.summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "summary", fileType: { description: "File type: 1=original(default) 2=HTML; only affects meeting platform summaries", choices: ["1", "2"] }, titleListEndpoint: "insight.summary.list" });
62
+ const PAMIRS_CATEGORIES = ["companyAnalysis", "industryAnalysis"];
63
+ const PAMIRS_MARKETS = ["aShares", "hkStocks", "usChinaConcept", "usStocks"];
64
+ query(pamirsSummary, "list", {
65
+ description: "List Pamirs expert summaries (requires the expert-summary database)",
66
+ endpoint: "insight.pamirs-summary.list",
67
+ cache: { endpointKey: "insight.pamirs-summary.list", idField: "summaryId" },
68
+ fields: [
69
+ searchType(), rankType(),
70
+ list("--research-area <id>", "Research area ID: accepts BOTH industry code sets -- citicIndustry (1008001xx) and swIndustry (104xx0000) -- but NOT gangtiseIndustry direction codes (122000xxx), which return 0 on this endpoint only", "researchAreaList"),
71
+ list("--security <code>", "Security code, e.g. 000001.SZ", "securityList"),
72
+ choiceList("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, "categoryList", PAMIRS_CATEGORIES),
73
+ choiceList("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, "marketList", PAMIRS_MARKETS),
74
+ format(), output("Output path"),
75
+ ...timeFilters(),
76
+ ],
77
+ });
78
+ addDownloadCommand(pamirsSummary, { endpointKey: "insight.pamirs-summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "pamirs-summary", fileType: { description: "File type: 1=original(default) 2=HTML", choices: ["1", "2"] }, titleListEndpoint: "insight.pamirs-summary.list" });
79
+ const addScheduleList = (command, endpointKey, fields) => query(command, "list", {
80
+ endpoint: endpointKey,
81
+ fields: [
82
+ fields.researchArea && list("--research-area <id>", RESEARCH_AREA_CITIC_OR_DIRECTION, "researchAreaList"),
83
+ fields.institution && list("--institution <id>", "Lead institution ID", "institutionList"),
84
+ fields.security && list("--security <code>", "Security code", "securityList"),
85
+ fields.object && list("--object <type>", "Object type: company/industry", "objectList"),
86
+ fields.category && list("--category <name>", fields.category, "categoryList"),
87
+ fields.market && list("--market <name>", fields.market, "marketList"),
88
+ fields.participantRole && list("--participant-role <name>", "Participant role: management/expert", "participantRoleList"),
89
+ fields.brokerType && list("--broker-type <name>", "Lead broker type: cnBroker/otherBroker", "brokerTypeList"),
90
+ fields.permission && numberList("--permission <number>", "Permission: 1=public 2=private", "permission"),
91
+ fields.location && list("--location <id>", "Location ID (domesticCity constant, via 'reference constant-list')", "locationList"),
92
+ format(), output("Output path"),
93
+ ...timeFilters(),
94
+ ].filter((f) => Boolean(f)),
95
+ });
96
+ addScheduleList(roadshow, "insight.roadshow.list", {
97
+ researchArea: true, institution: true, security: true, location: true,
98
+ category: "Roadshow type: earningsCall/strategyMeeting/companyAnalysis/industryAnalysis/fundRoadshow",
99
+ market: "Market: aShares/hkStocks/usChinaConcept/usStocks",
100
+ participantRole: true, brokerType: true, permission: true,
101
+ });
102
+ addScheduleList(siteVisit, "insight.site-visit.list", {
103
+ researchArea: true, institution: true, security: true, location: true, object: true,
104
+ category: "Site-visit form: single/series",
105
+ market: "Market: aShares/hkStocks/usChinaConcept",
106
+ permission: true,
107
+ });
108
+ addScheduleList(strategy, "insight.strategy.list", { institution: true, location: true });
109
+ addScheduleList(forum, "insight.forum.list", { researchArea: true, location: true });
110
+ const SECURITY_ONLY_ROW_CAP = 1000;
111
+ function flagIfImplicitCapHit(data, cap, from) {
112
+ if (!data || typeof data !== "object" || Array.isArray(data))
113
+ return;
114
+ const rec = data;
115
+ const rows = rowCount(data);
116
+ if (rows < cap)
117
+ return;
118
+ const total = typeof rec.total === "number" ? rec.total : undefined;
119
+ if (total !== undefined && from + rows >= total)
120
+ return;
121
+ rec.partial = true;
122
+ process.stderr.write(`[gangtise] warning: --security was the only bound, so the fetch was capped at ${cap} rows and more remain (total=${String(rec.total)}) — the filter may not have narrowed anything. Re-run with --start-date/--end-date or an explicit --size.\n`);
123
+ }
124
+ const PERFORMANCE_MARKETS = ["aShares", "hkStocks", "usChinaConcept", "usStocks"];
125
+ const PERFORMANCE_CATEGORIES = ["performanceForecast", "performanceExpress", "performanceAnnouncement"];
126
+ performanceCalendar.command("list").description("Earnings calendar (业绩预告 / 快报 / 公告)")
127
+ .option("--from <number>", "Starting offset", "0")
128
+ .option("--size <number>", "Total rows to return; omit to fetch all")
129
+ .option("--start-date <date>", "Start date (yyyy-MM-dd), filters publishDate", dateArg("--start-date"))
130
+ .option("--end-date <date>", "End date (yyyy-MM-dd), filters publishDate", dateArg("--end-date"))
131
+ .option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, [])
132
+ .addOption(multiChoice("--market <name>", `Market: ${PERFORMANCE_MARKETS.join("/")}`, PERFORMANCE_MARKETS))
133
+ .addOption(multiChoice("--category <name>", `Event type: ${PERFORMANCE_CATEGORIES.join("/")}`, PERFORMANCE_CATEGORIES))
134
+ .option("--format <format>", "Output format", "table").option("--output <path>", "Output path")
135
+ .addOption(confirmCostly().option)
136
+ .action((options) => {
137
+ const marketList = parseChoiceList(options.market, "--market", PERFORMANCE_MARKETS);
138
+ const categoryList = parseChoiceList(options.category, "--category", PERFORMANCE_CATEGORIES);
139
+ const explicitlyBounded = Boolean(options.size) || Boolean(options.startDate && options.endDate);
140
+ if (!explicitlyBounded && !options.security.length) {
141
+ throw new ValidationError("insight performance-calendar list without a bound would auto-paginate the whole calendar (over a hundred thousand rows at 0.1 credits each): pass --start-date and --end-date, or --security, or an explicit --size");
142
+ }
143
+ const implicitCap = explicitlyBounded ? undefined : SECURITY_ONLY_ROW_CAP;
144
+ const from = parseFrom(options.from);
145
+ return emit(options, async (client) => {
146
+ const data = await client.call("insight.performance-calendar.list", {
147
+ from, size: parseSize(options.size) ?? implicitCap,
148
+ startDate: options.startDate, endDate: options.endDate,
149
+ marketList,
150
+ securityList: maybeArray(options.security),
151
+ categoryList,
152
+ });
153
+ if (implicitCap)
154
+ flagIfImplicitCapHit(data, implicitCap, from);
155
+ return data;
156
+ }, { endpointKey: "insight.performance-calendar.list", idField: "performanceReportId" });
157
+ });
158
+ addDownloadCommand(performanceCalendar, { endpointKey: "insight.performance-calendar.download", idOption: "--performance-report-id", idField: "performanceReportId", fallbackPrefix: "performance-calendar", titleListEndpoint: "insight.performance-calendar.list" });
159
+ query(research, "list", {
160
+ endpoint: "insight.research.list",
161
+ cache: { endpointKey: "insight.research.list", idField: "reportId" },
162
+ fields: [
163
+ searchType(), rankType(),
164
+ list("--broker <id>", "Broker ID", "brokerList"),
165
+ list("--security <code>", "Security code", "securityList"),
166
+ list("--industry <id>", "Industry ID", "industryList"),
167
+ list("--category <name>", "Report category", "categoryList"),
168
+ list("--llm-tag <tag>", "Semantic tag", "llmTagList"),
169
+ list("--rating <name>", "Rating", "ratingList"),
170
+ list("--rating-change <name>", "Rating change", "ratingChangeList"),
171
+ count("--min-pages <number>", "Min report pages", "minReportPages"),
172
+ count("--max-pages <number>", "Max report pages", "maxReportPages"),
173
+ list("--source <type>", "Source type", "sourceList"),
174
+ format(), output("Output path"),
175
+ ...timeFilters(),
176
+ ],
177
+ });
178
+ addDownloadCommand(research, { endpointKey: "insight.research.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "research", fileType: { description: "File type: 1=PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.research.list" });
179
+ query(foreignReport, "list", {
180
+ endpoint: "insight.foreign-report.list",
181
+ cache: { endpointKey: "insight.foreign-report.list", idField: "reportId" },
182
+ fields: [
183
+ searchType(), rankType(),
184
+ list("--security <code>", "Security code", "securityList"),
185
+ list("--region <id>", "Region ID", "regionList"),
186
+ list("--category <name>", "Report category", "categoryList"),
187
+ list("--industry <id>", "Industry ID", "industryList"),
188
+ list("--broker <id>", "Broker ID", "brokerList"),
189
+ list("--llm-tag <tag>", "Semantic tag", "llmTagList"),
190
+ list("--rating <name>", "Rating", "ratingList"),
191
+ list("--rating-change <name>", "Rating change", "ratingChangeList"),
192
+ count("--min-pages <number>", "Min report pages", "minReportPages"),
193
+ count("--max-pages <number>", "Max report pages", "maxReportPages"),
194
+ format(), output("Output path"),
195
+ ...timeFilters(),
196
+ ],
197
+ });
198
+ addDownloadCommand(foreignReport, { endpointKey: "insight.foreign-report.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "foreign-report", fileType: { description: "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", choices: ["1", "2", "3", "4"], default: "1" }, titleListEndpoint: "insight.foreign-report.list" });
199
+ query(announcement, "list", {
200
+ endpoint: "insight.announcement.list",
201
+ cache: { endpointKey: "insight.announcement.list", idField: "announcementId" },
202
+ fields: [
203
+ searchType(), rankType(),
204
+ list("--security <code>", "Security code", "securityList"),
205
+ list("--category <id>", "Category ID", "categoryList"),
206
+ format(), output("Output path"),
207
+ ...timeFilters(parseTimestamp13),
208
+ ],
209
+ });
210
+ addDownloadCommand(announcement, { endpointKey: "insight.announcement.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement", fileType: { description: "File type: 1=PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement.list" });
211
+ query(announcementHk, "list", {
212
+ endpoint: "insight.announcement-hk.list",
213
+ cache: { endpointKey: "insight.announcement-hk.list", idField: "announcementId" },
214
+ fields: [
215
+ searchType(), rankType(),
216
+ list("--security <code>", "Security code (e.g. 01913.HK)", "securityList"),
217
+ list("--category <id>", "Category ID", "categoryList"),
218
+ format(), output("Output path"),
219
+ ...timeFilters(),
220
+ ],
221
+ });
222
+ addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", fileType: { description: "File type: 1=original 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-hk.list" });
223
+ query(announcementUs, "list", {
224
+ endpoint: "insight.announcement-us.list",
225
+ cache: { endpointKey: "insight.announcement-us.list", idField: "announcementId" },
226
+ fields: [
227
+ searchType(), rankType(),
228
+ list("--security <code>", "Security code (e.g. TSLA.O)", "securityList"),
229
+ list("--category <id>", "Category ID (constant-list usShareAnnouncementCategory)", "categoryList"),
230
+ format(), output("Output path"),
231
+ ...timeFilters(),
232
+ ],
233
+ });
234
+ addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
235
+ query(foreignOpinion, "list", {
236
+ endpoint: (options) => options.withContent ? "insight.foreign-opinion.list-with-content" : "insight.foreign-opinion.list",
237
+ fields: [
238
+ rankType(),
239
+ list("--security <code>", "Security code (e.g. UBER.N)", "securityList"),
240
+ list("--region <code>", "Region code -- this endpoint accepts only cn/cnHk/cnTw/us/jp/uk; the other 13 values of regionCategory (sea/gl/fr/de/kr/in/ca/me/othAs/othEur/latAm/oce/af) are rejected here with 100005 though they all work on foreign-report", "regionList"),
241
+ list("--industry <id>", SW_INDUSTRY_ONLY, "industryList"),
242
+ list("--broker <id>", "Broker ID", "brokerList"),
243
+ list("--rating <name>", "Rating", "ratingList"),
244
+ list("--rating-change <name>", "Rating change", "ratingChangeList"),
245
+ flag("--with-content", "Return the full body inline (v1 list, 30 credits/row) as content / contentTranslate, instead of the 200-char brief (1 credit/row)"),
246
+ format(), output("Output path"),
247
+ ...timeFilters(),
248
+ ],
249
+ });
250
+ opinion.command("detail").description("Full bodies of domestic chief opinions by ID (30 credits per returned opinion; batched 20 per call)")
251
+ .requiredOption("--chief-opinion-id <id>", "chiefOpinionId from 'insight opinion list' (repeat or comma-separate)", collectList)
252
+ .option("--format <format>", "Output format", "json").option("--output <path>")
253
+ .action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.opinion.detail", "chiefOpinionIdList", "chiefOpinionId", options.chiefOpinionId)));
254
+ foreignOpinion.command("detail").description("Full bodies (content + contentTranslate) of foreign opinions by ID (30 credits per returned opinion; batched 20 per call)")
255
+ .requiredOption("--foreign-opinion-id <id>", "foreignOpinionId from 'insight foreign-opinion list' (repeat or comma-separate)", collectList)
256
+ .option("--format <format>", "Output format", "json").option("--output <path>")
257
+ .action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.foreign-opinion.detail", "foreignOpinionIdList", "foreignOpinionId", options.foreignOpinionId)));
258
+ query(independentOpinion, "list", {
259
+ endpoint: "insight.independent-opinion.list",
260
+ fields: [
261
+ rankType(),
262
+ list("--security <code>", "Security code (e.g. GSK.N)", "securityList"),
263
+ list("--industry <id>", SW_INDUSTRY_ONLY, "industryList"),
264
+ list("--rating <name>", "Rating", "ratingList"),
265
+ list("--rating-change <name>", "Rating change", "ratingChangeList"),
266
+ format(), output("Output path"),
267
+ ...timeFilters(),
268
+ ],
269
+ });
270
+ addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", choices: ["1", "2"], required: true } });
271
+ query(officialAccount, "list", {
272
+ endpoint: "insight.official-account.list",
273
+ cache: { endpointKey: "insight.official-account.list", idField: "articleId" },
274
+ fields: [
275
+ searchType(), rankType(),
276
+ list("--account-id <id>", "Official account ID", "accountIdList"),
277
+ list("--security <code>", "Security code (e.g. 000001.SZ)", "securityList"),
278
+ list("--category <type>", "Article type: news/law/report/view/data/event/meeting/notice/recruit/investEdu/brand/notes/other", "categoryList"),
279
+ list("--industry <id>", "Industry ID (constant-list citicIndustry/swIndustry)", "industryList"),
280
+ format(), output("Output path"),
281
+ ...timeFilters(),
282
+ ],
283
+ });
284
+ addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.download", idOption: "--article-id", idField: "articleId", fallbackPrefix: "official-account", fileType: { description: "File type: 1=txt(default) 2=HTML", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.official-account.list" });
285
+ query(qa, "list", {
286
+ endpoint: "insight.qa.list",
287
+ fields: [
288
+ required("--security-code <code>", "Security code, e.g. 601012.SH", "securityCode"),
289
+ from(), size("Total rows to return; omit to fetch all (max page 500)"),
290
+ startTime("Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"), endTime("End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"),
291
+ list("--source <type>", "Source: conference/interactive/survey (repeat)", "source"),
292
+ list("--question-category <name>", "Question category (repeat): productAndBusiness/capacityAndProjects/ordersAndCustomers/financialData/materialEvents/capitalOperations/shareholdersAndDividends/corporateGovernance/marketAndValuation/macroAndIndustry/risksAndOthers", "questionCategory"),
293
+ numberList("--answer-important <flag>", "Answer involves key info: 1=yes 0=no (repeat; omit for all)", "answerImportant"),
294
+ format(), output(),
295
+ ],
296
+ });
297
+ query(reportImage, "list", {
298
+ endpoint: "insight.report-image.list",
299
+ fields: [
300
+ required("--keyword <text>", "Search keyword, e.g. 'AI' '新能源汽车'", "keyword"),
301
+ top(20),
302
+ value("--source-id <id>", "Report source ID, to filter to one report (from a report list or knowledge base)", "sourceId"),
303
+ startTime("Start time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)"), endTime("End time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)"),
304
+ format(), output(),
305
+ ],
306
+ });
307
+ addDownloadCommand(reportImage, { endpointKey: "insight.report-image.download", idOption: "--chunk-id", idField: "chunkId", fallbackPrefix: "report-image" });
308
+ insight.addCommand(opinion);
309
+ insight.addCommand(summary);
310
+ insight.addCommand(pamirsSummary);
311
+ insight.addCommand(roadshow);
312
+ insight.addCommand(siteVisit);
313
+ insight.addCommand(strategy);
314
+ insight.addCommand(forum);
315
+ insight.addCommand(performanceCalendar);
316
+ insight.addCommand(research);
317
+ insight.addCommand(foreignReport);
318
+ insight.addCommand(announcement);
319
+ insight.addCommand(announcementHk);
320
+ insight.addCommand(announcementUs);
321
+ insight.addCommand(foreignOpinion);
322
+ insight.addCommand(independentOpinion);
323
+ insight.addCommand(officialAccount);
324
+ insight.addCommand(qa);
325
+ insight.addCommand(reportImage);
326
+ const highlight = new Command("highlight");
327
+ query(highlight, "list", {
328
+ description: "Meeting highlights feed: key takeaways per meeting, newest first (5 credits/row)",
329
+ endpoint: "insight.highlight.list",
330
+ fields: [
331
+ from(), size("Total rows to return; omit to fetch all (max page 50). Billed per row — bound this on wide ranges"),
332
+ startTime("Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"), endTime("End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)"),
333
+ list("--security <code>", "Security code, e.g. 000001.SZ / 09992.HK / AAPL.O (repeatable)", "securityList"),
334
+ list("--research-area <id>", "Research area ID: citicIndustry (1008001xx) or gangtiseIndustry direction (122000xxx). swIndustry codes are NOT accepted here", "researchAreaList"),
335
+ format(), output(),
336
+ ],
337
+ });
338
+ insight.addCommand(highlight);
@@ -0,0 +1,181 @@
1
+ import { Command } from "commander";
2
+ import { collectList, dateArg, datetimeArg, maybeArray, parseOptionalNumberOption } from "../core/args.js";
3
+ import { buildQuoteKlineBody } from "../core/commandBodies.js";
4
+ import { callPerSecurity, estimateTradingDays } from "../core/perSecurity.js";
5
+ import { callKlineWithSharding, isFullMarket } from "../core/quoteSharding.js";
6
+ import { ValidationError } from "../core/errors.js";
7
+ import { flagMissingFields } from "../core/normalize.js";
8
+ import { parseOutputFormat } from "../core/output.js";
9
+ import { printData } from "../core/printer.js";
10
+ import { rowCount } from "../core/rowSink.js";
11
+ import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart } from "./shared.js";
12
+ export const quote = new Command("quote").description("Quote APIs");
13
+ const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
14
+ const LEGACY_ALL_MARKET = (shardDays) => ({ all: shardDays });
15
+ const REALTIME_MARKETS = ["aShares", "hkStocks", "usStocks"];
16
+ const FUND_FLOW_MARKETS = ["aShares"];
17
+ const MARKET_KEYWORDS = new Set(["all", "ashares", "hkstocks", "usstocks"]);
18
+ const matchesMarketKeyword = (value, keyword) => value.toLowerCase() === keyword.toLowerCase();
19
+ export const checkMarketKeywords = (securities, accepted, command, noKeywordReason) => {
20
+ const used = securities.filter((s) => MARKET_KEYWORDS.has(s.toLowerCase()));
21
+ if (used.length === 0)
22
+ return;
23
+ const unsupported = used.filter((k) => !accepted.some((a) => matchesMarketKeyword(k, a)));
24
+ if (unsupported.length > 0) {
25
+ throw new ValidationError(accepted.length > 0
26
+ ? `${command}: '${unsupported[0]}' is not a whole-market keyword for this command — use ${accepted.join(" / ")}`
27
+ : `${command}: this command takes explicit security codes only — '${unsupported[0]}' and other whole-market keywords are not supported${noKeywordReason ? `: ${noKeywordReason}` : ""}`);
28
+ }
29
+ if (securities.length > 1) {
30
+ throw new ValidationError(`${command}: a market keyword must be passed alone, got '${securities.join(", ")}' — the API rejects it mixed with security codes or another keyword`);
31
+ }
32
+ };
33
+ const canonicalizeMarketKeywords = (securities, accepted) => securities.map((s) => accepted.find((a) => matchesMarketKeyword(s, a)) ?? s);
34
+ function withIdentityFields(fieldList, identity, label) {
35
+ if (!fieldList)
36
+ return fieldList;
37
+ const missing = identity.filter((field) => !fieldList.includes(field));
38
+ if (missing.length === 0)
39
+ return fieldList;
40
+ const says = [missing.includes("securityCode") ? "security" : "", missing.some((f) => f !== "securityCode") ? "time" : ""].filter(Boolean).join(" and ");
41
+ process.stderr.write(`[gangtise] note: ${label} adds ${missing.join(", ")} to --field so each row says which ${says} it belongs to.\n`);
42
+ return [...missing, ...fieldList];
43
+ }
44
+ const addKlineCommand = (name, endpointKey, securityHelp, markets, noKeywordReason) => quote.command(name)
45
+ .option("--security <code>", securityHelp, collectList, [])
46
+ .option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
47
+ .option("--end-date <date>", "End date (default: latest)", dateArg("--end-date"))
48
+ .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). With several securities it also sizes the groups they are fetched in, so a small value fetches one security per request`)
49
+ .option("--field <field>", "Field", collectList, [])
50
+ .option("--format <format>", "Output format", "table")
51
+ .option("--output <path>")
52
+ .action((options) => {
53
+ checkMarketKeywords(options.security, Object.keys(markets), `quote ${name}`, noKeywordReason);
54
+ options.security = canonicalizeMarketKeywords(options.security, Object.keys(markets));
55
+ const fullMarketKeyword = Object.keys(markets).find((k) => options.security.includes(k));
56
+ if (fullMarketKeyword && (!options.startDate || !options.endDate)) {
57
+ throw new ValidationError(`quote ${name} --security ${fullMarketKeyword} requires both --start-date and --end-date (the full market is fetched via date shards; an unbounded whole-market request is rejected by the server)`);
58
+ }
59
+ return withClient(options, async (client) => {
60
+ const format = parseOutputFormat(options.format);
61
+ client.rowSink?.watchEarliest("tradeDate");
62
+ const body = buildQuoteKlineBody(options);
63
+ const severalSecurities = (body.securityList?.length ?? 0) > 1 || fullMarketKeyword !== undefined;
64
+ body.fieldList = withIdentityFields(body.fieldList, severalSecurities ? ["securityCode", "tradeDate"] : [], `quote ${name}`);
65
+ const keyword = Object.keys(markets).find((k) => isFullMarket(body, k));
66
+ if (keyword) {
67
+ const data = await callKlineWithSharding(client, endpointKey, body, { shardDays: markets[keyword], fullMarketValue: keyword });
68
+ flagMissingFields(data, body.fieldList, `quote ${name}`);
69
+ noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
70
+ await printData(data, format, options.output);
71
+ return;
72
+ }
73
+ const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
74
+ const securities = body.securityList ?? [];
75
+ const tradingDays = estimateTradingDays(body.startDate, body.endDate);
76
+ if (securities.length > 1 && securities.length * tradingDays >= limit) {
77
+ const cap = body.limit ?? MAX_QUOTE_LIMIT;
78
+ const groupSize = Math.max(1, Math.floor((cap - 1) / tradingDays));
79
+ const data = await callPerSecurity(client, endpointKey, securities, (codes) => ({ ...body, securityList: codes, limit: cap }), cap, `quote ${name}`, groupSize);
80
+ flagMissingFields(data, body.fieldList, `quote ${name}`);
81
+ noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
82
+ await printData(data, format, options.output);
83
+ return;
84
+ }
85
+ const data = await client.call(endpointKey, { ...body, limit });
86
+ flagIfLimitTruncated(data, limit, name);
87
+ flagMissingFields(data, body.fieldList, `quote ${name}`);
88
+ noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
89
+ await printData(data, format, options.output);
90
+ });
91
+ });
92
+ addKlineCommand("day-kline", "quote.day-kline", "Security code — A-share .SH/.SZ/.BJ, ETF .SH/.SZ (e.g. 512800.SH), HK .HK, US .O/.N/.A, exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); or one market keyword: aShares / hkStocks / usStocks (auto-sharded by date, must be passed alone; keywords cover stocks only — ETFs and indices must be listed by code)", KLINE_MARKETS);
93
+ addKlineCommand("day-kline-hk", "quote.day-kline-hk", "[deprecated: use 'day-kline'] Security code (HK stock: .HK, or 'all' for full market)", LEGACY_ALL_MARKET(2));
94
+ addKlineCommand("day-kline-us", "quote.day-kline-us", "[deprecated: use 'day-kline'] Security code (US stock: e.g. AAPL.O, or 'all' for full market)", LEGACY_ALL_MARKET(1));
95
+ addKlineCommand("index-day-kline", "quote.index-day-kline", "[deprecated: use 'day-kline'] Index code (.SH/.SZ/.BJ), repeat for several; no whole-market keyword — 'all' is refused because the endpoint answers it with an empty result", {}, "the endpoint answers 'all' with an empty result. List the index codes one by one — quote day-kline takes the same codes");
96
+ quote.command("minute-kline")
97
+ .option("--security <code>", "Security code — A-share .SH/.SZ (SH/SZ only), ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); repeat for several — one request each, run concurrently and merged; no whole-market keyword", collectList, [])
98
+ .option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time"))
99
+ .option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time"))
100
+ .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT})`)
101
+ .option("--field <field>", "Field", collectList, [])
102
+ .option("--format <format>", "Output format", "table")
103
+ .option("--output <path>")
104
+ .action((options) => withClient(options, async (client) => {
105
+ const format = parseOutputFormat(options.format);
106
+ client.rowSink?.watchEarliest("tradeTime");
107
+ const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }) ?? DEFAULT_QUOTE_LIMIT;
108
+ const securities = options.security;
109
+ if (securities.length === 0)
110
+ throw new ValidationError("--security is required (repeat it for several securities)");
111
+ const fieldList = withIdentityFields(maybeArray(options.field), securities.length > 1 ? ["securityCode", "tradeTime"] : [], "quote minute-kline");
112
+ const makeBody = (code) => ({ securityCode: code, startTime: options.startTime, endTime: options.endTime, limit, fieldList });
113
+ const data = securities.length === 1
114
+ ? await client.call("quote.minute-kline", makeBody(securities[0]))
115
+ : await callPerSecurity(client, "quote.minute-kline", securities, ([code]) => makeBody(code), limit, "quote minute-kline");
116
+ if (securities.length === 1)
117
+ flagIfLimitTruncated(data, limit, "minute-kline", "--start-time/--end-time");
118
+ flagMissingFields(data, fieldList, "quote minute-kline");
119
+ noteLateStart(data, options.startTime, "tradeTime", "quote minute-kline");
120
+ if (rowCount(data) === 0 && (options.startTime || options.endTime)) {
121
+ process.stderr.write(`[gangtise] note: no minute bars came back. Minute bars cover a much shorter history than daily bars, and a range entirely before it returns nothing rather than an error — try a recent range to check.\n`);
122
+ }
123
+ await printData(data, format, options.output);
124
+ }));
125
+ quote.command("realtime")
126
+ .description("Realtime quote snapshot (A-share / HK / US stocks, ETFs, and indices incl. global indices)")
127
+ .option("--security <code>", "Security code — stock .SH/.SZ/.BJ/.HK/.O/.N/.A, ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); or one market keyword: aShares / hkStocks / usStocks (must be passed alone; keywords cover stocks only — ETFs and indices have no whole-market keyword)", collectList, [])
128
+ .option("--field <field>", "Field", collectList, [])
129
+ .option("--format <format>", "Output format", "table")
130
+ .option("--output <path>")
131
+ .action((options) => {
132
+ checkMarketKeywords(options.security, REALTIME_MARKETS, "quote realtime");
133
+ return emit(options, async (client) => {
134
+ const securities = options.security;
135
+ const severalSecurities = securities.length > 1 || securities.some((s) => REALTIME_MARKETS.some((keyword) => matchesMarketKeyword(s, keyword)));
136
+ const fieldList = withIdentityFields(maybeArray(options.field), severalSecurities ? ["securityCode"] : [], "quote realtime");
137
+ const data = await client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList });
138
+ flagMissingFields(data, fieldList, "quote realtime");
139
+ return data;
140
+ });
141
+ });
142
+ quote.command("fund-flow")
143
+ .description("A-share daily fund flow (SH/SZ/BJ)")
144
+ .option("--security <code>", "Security code (e.g. 600519.SH / 920982.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, [])
145
+ .option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)", dateArg("--start-date"))
146
+ .option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)", dateArg("--end-date"))
147
+ .option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}; single-security cap — aShares auto-shards by day)`)
148
+ .option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, [])
149
+ .option("--format <format>", "Output format", "table")
150
+ .option("--output <path>")
151
+ .action((options) => {
152
+ checkMarketKeywords(options.security, FUND_FLOW_MARKETS, "quote fund-flow");
153
+ options.security = canonicalizeMarketKeywords(options.security, FUND_FLOW_MARKETS);
154
+ return withClient(options, async (client) => {
155
+ const format = parseOutputFormat(options.format);
156
+ client.rowSink?.watchEarliest("tradeDate");
157
+ const body = {
158
+ securityList: maybeArray(options.security),
159
+ startDate: options.startDate,
160
+ endDate: options.endDate,
161
+ limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }),
162
+ fieldList: maybeArray(options.field),
163
+ };
164
+ if (isFullMarket(body, "aShares")) {
165
+ if (!body.startDate || !body.endDate) {
166
+ throw new ValidationError("quote fund-flow --security aShares requires both --start-date and --end-date (the full market is fetched via per-day shards)");
167
+ }
168
+ const data = await callKlineWithSharding(client, "quote.fund-flow", body, { shardDays: 1, fullMarketValue: "aShares" });
169
+ flagMissingFields(data, body.fieldList, "quote fund-flow");
170
+ noteLateStart(data, body.startDate, "tradeDate", "quote fund-flow");
171
+ await printData(data, format, options.output);
172
+ return;
173
+ }
174
+ const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
175
+ const data = await client.call("quote.fund-flow", { ...body, limit });
176
+ flagIfLimitTruncated(data, limit, "fund-flow");
177
+ flagMissingFields(data, body.fieldList, "quote fund-flow");
178
+ noteLateStart(data, body.startDate, "tradeDate", "quote fund-flow");
179
+ await printData(data, format, options.output);
180
+ });
181
+ });
@@ -0,0 +1,49 @@
1
+ import { Command } from "commander";
2
+ import { collectKeyValue } from "../core/args.js";
3
+ import { ENDPOINTS, listEndpoints } from "../core/endpoints.js";
4
+ import { ConfigError, ValidationError } from "../core/errors.js";
5
+ import { flagFailedItems } from "../core/normalize.js";
6
+ import { parseOutputFormat } from "../core/output.js";
7
+ import { printData } from "../core/printer.js";
8
+ import { createClient, runDownload, assertConfirmed } from "./shared.js";
9
+ export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
10
+ const endpoint = ENDPOINTS[endpointKey];
11
+ if (!endpoint) {
12
+ throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
13
+ }
14
+ assertConfirmed(endpointKey, Boolean(options.yes), endpointKey);
15
+ const format = parseOutputFormat(options.format);
16
+ const client = await createClient({ format, output: options.output, yes: Boolean(options.yes) });
17
+ let body;
18
+ if (options.body) {
19
+ try {
20
+ body = JSON.parse(options.body);
21
+ }
22
+ catch {
23
+ throw new ConfigError(`Invalid JSON in --body: ${options.body}`);
24
+ }
25
+ }
26
+ if (endpoint.kind === "download") {
27
+ if (body !== undefined && endpoint.method !== "POST") {
28
+ throw new ValidationError(`--body is not supported for GET download endpoints (use --query key=value); ${endpointKey} is kind=download`);
29
+ }
30
+ await runDownload(client, endpointKey, options.query, {
31
+ output: options.output,
32
+ fallbackName: "download.bin",
33
+ body,
34
+ });
35
+ return;
36
+ }
37
+ if (Object.keys(options.query).length > 0) {
38
+ throw new ValidationError(`--query is not supported for JSON endpoints (use --body '{...}'); ${endpointKey} is kind=json`);
39
+ }
40
+ try {
41
+ const data = await client.call(endpointKey, body);
42
+ if (endpoint.itemFailures)
43
+ flagFailedItems(data, `raw call ${endpointKey}`);
44
+ await printData(data, format, options.output);
45
+ }
46
+ finally {
47
+ await client.rowSink?.abort();
48
+ }
49
+ })).addCommand(new Command("list").description("List all registered endpoint keys (for use with 'raw call')").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => printData(listEndpoints(), parseOutputFormat(options.format), options.output)));
@@ -0,0 +1,58 @@
1
+ import { Command } from "commander";
2
+ import { value, required, choiceList, top, format, output, query } from "./shared.js";
3
+ export const reference = new Command("reference").description("Reference data APIs");
4
+ query(reference, "securities-search", {
5
+ endpoint: "reference.securities-search",
6
+ fields: [
7
+ required("--keyword <text>", "Search keyword (name/code/pinyin/English)", "keyword"),
8
+ choiceList("--category <type>", "Category: stock/dr/index/fund", "category", ["stock", "dr", "index", "fund"]),
9
+ top(10),
10
+ format(), output(),
11
+ ],
12
+ });
13
+ query(reference, "constant-category", {
14
+ description: "List constant categories and which API params accept them",
15
+ endpoint: "reference.constant-category",
16
+ fields: [format(), output()],
17
+ });
18
+ query(reference, "constant-list", {
19
+ endpoint: "reference.constant-list",
20
+ fields: [
21
+ required("--category <code>", "Category code from 'reference constant-category' (e.g. citicIndustry/swIndustry/regionCategory)", "category"),
22
+ format(), output(),
23
+ ],
24
+ });
25
+ query(reference, "concept-search", {
26
+ endpoint: "reference.concept-search",
27
+ fields: [required("--keyword <text>", "Search keyword (name/pinyin/group name)", "keyword"), top(10), format(), output()],
28
+ });
29
+ query(reference, "sector-search", {
30
+ endpoint: "reference.sector-search",
31
+ fields: [value("--keyword <text>", "Search keyword (name/pinyin)", "keyword"), top(10), format(), output()],
32
+ });
33
+ query(reference, "sector-constituents", {
34
+ endpoint: "reference.sector-constituents",
35
+ fields: [required("--sector-id <id>", "Sector ID from 'reference sector-search'", "sectorId"), format(), output()],
36
+ });
37
+ query(reference, "chiefs-search", {
38
+ endpoint: "reference.chiefs-search",
39
+ fields: [required("--keyword <text>", "Search keyword (chief name / institution / team)", "keyword"), top(10), format(), output()],
40
+ });
41
+ query(reference, "institution-search", {
42
+ endpoint: "reference.institution-search",
43
+ fields: [
44
+ required("--keyword <text>", "Search keyword (institution name / abbreviation)", "keyword"),
45
+ choiceList("--category <name>", "Category: domesticBroker/foreignInstitution/leadInstitution/opinionInstitution/foreignOpinionInstitution (repeat); omit for all", "categoryList", ["domesticBroker", "foreignInstitution", "leadInstitution", "opinionInstitution", "foreignOpinionInstitution"]),
46
+ top(10),
47
+ format(), output(),
48
+ ],
49
+ });
50
+ query(reference, "official-account-search", {
51
+ endpoint: "reference.official-account-search",
52
+ fields: [
53
+ required("--keyword <text>", "Search keyword (account name / institution / keyword, e.g. 东吴证券)", "keyword"),
54
+ choiceList("--category <name>", "Category: listedCompany/broker/government/media (repeat); omit for all incl. uncategorized", "category", ["listedCompany", "broker", "government", "media"]),
55
+ top(10),
56
+ format(), output(),
57
+ ],
58
+ });