gangtise-openapi-cli 0.50.0 → 0.52.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 +94 -49
- package/dist/src/cli.js +29 -8
- package/dist/src/commands/ai.js +27 -12
- package/dist/src/commands/alternative.js +2 -2
- package/dist/src/commands/bond.js +4 -4
- package/dist/src/commands/fund.js +1 -1
- package/dist/src/commands/fundamental.js +11 -11
- package/dist/src/commands/indicator.js +5 -5
- package/dist/src/commands/insight.js +38 -17
- package/dist/src/commands/quote.js +6 -6
- package/dist/src/commands/raw.js +3 -3
- package/dist/src/commands/reference.js +35 -3
- package/dist/src/commands/shared.js +20 -8
- package/dist/src/commands/tool.js +9 -4
- package/dist/src/commands/update.js +11 -0
- package/dist/src/commands/vault.js +17 -16
- package/dist/src/core/client.js +21 -4
- package/dist/src/core/download.js +12 -3
- package/dist/src/core/driveUpload.js +6 -2
- package/dist/src/core/dryRun.js +68 -0
- package/dist/src/core/endpoints.js +87 -6
- package/dist/src/core/errors.js +2 -2
- package/dist/src/core/etfComponents.js +65 -8
- package/dist/src/core/exitStatus.js +4 -0
- package/dist/src/core/fileParse.js +10 -3
- package/dist/src/core/printer.js +39 -7
- package/dist/src/core/statePaths.js +41 -0
- package/dist/src/core/transport.js +7 -0
- package/gangtise-openapi/SKILL.md +43 -156
- package/gangtise-openapi/references/billing.md +142 -0
- package/gangtise-openapi/references/commands/ai.md +26 -0
- package/gangtise-openapi/references/commands/fund.md +3 -3
- package/gangtise-openapi/references/commands/fundamental.md +2 -1
- package/gangtise-openapi/references/commands/indicator.md +106 -15
- package/gangtise-openapi/references/commands/insight.md +31 -6
- package/gangtise-openapi/references/commands/reference-and-lookup.md +42 -4
- package/gangtise-openapi/references/commands/tool.md +2 -2
- package/gangtise-openapi/references/commands/vault.md +26 -4
- package/gangtise-openapi/references/errors.md +3 -3
- package/gangtise-openapi/references/examples.md +3 -3
- package/gangtise-openapi/references/lookup-ids.md +13 -0
- package/gangtise-openapi/references/response-schema.md +8 -2
- package/package.json +2 -2
|
@@ -71,7 +71,7 @@ const ENDPOINT_DEFS = {
|
|
|
71
71
|
path: "/application/open-insight/chief-opinion/getDetail",
|
|
72
72
|
kind: "json",
|
|
73
73
|
description: "Get domestic chief opinion bodies by ID (max 20 IDs per call)",
|
|
74
|
-
billing: { per: "row", price: 30, maxUnits: 20 },
|
|
74
|
+
billing: { per: "row", price: 30, maxUnits: 20, unitsFrom: ["chiefOpinionIdList"] },
|
|
75
75
|
retry: "no-replay",
|
|
76
76
|
expects: "array",
|
|
77
77
|
},
|
|
@@ -267,7 +267,7 @@ const ENDPOINT_DEFS = {
|
|
|
267
267
|
path: "/application/open-insight/foreign-opinion/getDetail",
|
|
268
268
|
kind: "json",
|
|
269
269
|
description: "Get foreign opinion bodies by ID (max 20 IDs per call)",
|
|
270
|
-
billing: { per: "row", price: 30, maxUnits: 20 },
|
|
270
|
+
billing: { per: "row", price: 30, maxUnits: 20, unitsFrom: ["foreignOpinionIdList"] },
|
|
271
271
|
retry: "no-replay",
|
|
272
272
|
expects: "array",
|
|
273
273
|
},
|
|
@@ -288,6 +288,25 @@ const ENDPOINT_DEFS = {
|
|
|
288
288
|
description: "Download foreign independent opinion file",
|
|
289
289
|
billing: { per: "document", price: 30 },
|
|
290
290
|
},
|
|
291
|
+
"insight.overseas-opinion.list": {
|
|
292
|
+
method: "POST",
|
|
293
|
+
path: "/application/open-insight/overseasOpinion/getList",
|
|
294
|
+
kind: "json",
|
|
295
|
+
description: "List overseas opinions (brief only; body via detail)",
|
|
296
|
+
billing: { per: "row", price: 0.1 },
|
|
297
|
+
pagination: { enabled: true, maxPageSize: 50, maxWindow: 10000 },
|
|
298
|
+
rowId: "overseasOpinionId",
|
|
299
|
+
expects: "list",
|
|
300
|
+
},
|
|
301
|
+
"insight.overseas-opinion.detail": {
|
|
302
|
+
method: "POST",
|
|
303
|
+
path: "/application/open-insight/overseasOpinion/getDetail",
|
|
304
|
+
kind: "json",
|
|
305
|
+
description: "Get overseas opinion bodies by ID (max 20 IDs per call)",
|
|
306
|
+
billing: { per: "row", price: 10, maxUnits: 20, unitsFrom: ["overseasOpinionIdList"] },
|
|
307
|
+
retry: "no-replay",
|
|
308
|
+
expects: "array",
|
|
309
|
+
},
|
|
291
310
|
"insight.official-account.list": {
|
|
292
311
|
method: "POST",
|
|
293
312
|
path: "/application/open-insight/officialAccount/getList",
|
|
@@ -317,6 +336,7 @@ const ENDPOINT_DEFS = {
|
|
|
317
336
|
path: "/application/open-insight/report-image/getList",
|
|
318
337
|
kind: "json",
|
|
319
338
|
description: "Search research report images by keyword (returns chunkId + metadata)",
|
|
339
|
+
expects: "array",
|
|
320
340
|
},
|
|
321
341
|
"insight.highlight.list": {
|
|
322
342
|
method: "POST",
|
|
@@ -341,54 +361,79 @@ const ENDPOINT_DEFS = {
|
|
|
341
361
|
path: "/application/open-reference/securities/search",
|
|
342
362
|
kind: "json",
|
|
343
363
|
description: "Search GTS codes (securities)",
|
|
364
|
+
expects: "list",
|
|
344
365
|
},
|
|
345
366
|
"reference.chiefs-search": {
|
|
346
367
|
method: "POST",
|
|
347
368
|
path: "/application/open-reference/chiefs/search",
|
|
348
369
|
kind: "json",
|
|
349
370
|
description: "Search chief analyst IDs by name / institution / team",
|
|
371
|
+
expects: "list",
|
|
350
372
|
},
|
|
351
373
|
"reference.institution-search": {
|
|
352
374
|
method: "POST",
|
|
353
375
|
path: "/application/open-reference/institutions/search",
|
|
354
376
|
kind: "json",
|
|
355
377
|
description: "Search institution IDs by keyword (domestic broker / foreign / lead / opinion institution)",
|
|
378
|
+
expects: "list",
|
|
356
379
|
},
|
|
357
380
|
"reference.official-account-search": {
|
|
358
381
|
method: "POST",
|
|
359
382
|
path: "/application/open-reference/officialAccount/search",
|
|
360
383
|
kind: "json",
|
|
361
384
|
description: "Search official account (WeChat public account) IDs by name / institution / category",
|
|
385
|
+
expects: "list",
|
|
386
|
+
},
|
|
387
|
+
"reference.overseas-account-search": {
|
|
388
|
+
method: "POST",
|
|
389
|
+
path: "/application/open-reference/overseasAccount/search",
|
|
390
|
+
kind: "json",
|
|
391
|
+
description: "Search overseas opinion account IDs by name fragment",
|
|
392
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
393
|
+
rowId: "accountId",
|
|
394
|
+
expects: "list",
|
|
362
395
|
},
|
|
363
396
|
"reference.constant-category": {
|
|
364
397
|
method: "GET",
|
|
365
398
|
path: "/application/open-reference/constants/category",
|
|
366
399
|
kind: "json",
|
|
367
400
|
description: "List constant categories and their API usage scopes",
|
|
401
|
+
expects: "list",
|
|
368
402
|
},
|
|
369
403
|
"reference.constant-list": {
|
|
370
404
|
method: "POST",
|
|
371
405
|
path: "/application/open-reference/constants/getList",
|
|
372
406
|
kind: "json",
|
|
373
407
|
description: "List all constant values of a category",
|
|
408
|
+
expects: "object",
|
|
374
409
|
},
|
|
375
410
|
"reference.concept-search": {
|
|
376
411
|
method: "POST",
|
|
377
412
|
path: "/application/open-reference/concepts/search",
|
|
378
413
|
kind: "json",
|
|
379
414
|
description: "Search concept (theme) IDs by keyword",
|
|
415
|
+
expects: "list",
|
|
380
416
|
},
|
|
381
417
|
"reference.sector-search": {
|
|
382
418
|
method: "POST",
|
|
383
419
|
path: "/application/open-reference/sectors/search",
|
|
384
420
|
kind: "json",
|
|
385
421
|
description: "Search sector IDs by keyword",
|
|
422
|
+
expects: "list",
|
|
386
423
|
},
|
|
387
424
|
"reference.sector-constituents": {
|
|
388
425
|
method: "POST",
|
|
389
426
|
path: "/application/open-reference/sectors/constituents",
|
|
390
427
|
kind: "json",
|
|
391
428
|
description: "List constituent securities of a sector",
|
|
429
|
+
expects: "list",
|
|
430
|
+
},
|
|
431
|
+
"reference.index-weight": {
|
|
432
|
+
method: "POST",
|
|
433
|
+
path: "/application/open-quote/index/constituent-weight",
|
|
434
|
+
kind: "json",
|
|
435
|
+
description: "List index constituents with their weights (%) on one trading date",
|
|
436
|
+
expects: "list",
|
|
392
437
|
},
|
|
393
438
|
"quote.day-kline": {
|
|
394
439
|
method: "POST",
|
|
@@ -451,72 +496,84 @@ const ENDPOINT_DEFS = {
|
|
|
451
496
|
path: "/application/open-fundamental/financial-report/income-statement/accumulated",
|
|
452
497
|
kind: "json",
|
|
453
498
|
description: "Query A-share income statement (accumulated)",
|
|
499
|
+
expects: "list",
|
|
454
500
|
},
|
|
455
501
|
"fundamental.income-statement-quarterly": {
|
|
456
502
|
method: "POST",
|
|
457
503
|
path: "/application/open-fundamental/financial-report/income-statement/quarterly",
|
|
458
504
|
kind: "json",
|
|
459
505
|
description: "Query A-share income statement (quarterly)",
|
|
506
|
+
expects: "list",
|
|
460
507
|
},
|
|
461
508
|
"fundamental.balance-sheet": {
|
|
462
509
|
method: "POST",
|
|
463
510
|
path: "/application/open-fundamental/financial-report/balance-sheet/accumulated",
|
|
464
511
|
kind: "json",
|
|
465
512
|
description: "Query A-share balance sheet (accumulated)",
|
|
513
|
+
expects: "list",
|
|
466
514
|
},
|
|
467
515
|
"fundamental.cash-flow": {
|
|
468
516
|
method: "POST",
|
|
469
517
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/accumulated",
|
|
470
518
|
kind: "json",
|
|
471
519
|
description: "Query A-share cash flow statement (accumulated)",
|
|
520
|
+
expects: "list",
|
|
472
521
|
},
|
|
473
522
|
"fundamental.cash-flow-quarterly": {
|
|
474
523
|
method: "POST",
|
|
475
524
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/quarterly",
|
|
476
525
|
kind: "json",
|
|
477
526
|
description: "Query A-share cash flow statement (quarterly)",
|
|
527
|
+
expects: "list",
|
|
478
528
|
},
|
|
479
529
|
"fundamental.income-statement-hk": {
|
|
480
530
|
method: "POST",
|
|
481
531
|
path: "/application/open-fundamental/financial-report/income-statement/hk",
|
|
482
532
|
kind: "json",
|
|
483
533
|
description: "Query HK income statement (China GAAP)",
|
|
534
|
+
expects: "list",
|
|
484
535
|
},
|
|
485
536
|
"fundamental.balance-sheet-hk": {
|
|
486
537
|
method: "POST",
|
|
487
538
|
path: "/application/open-fundamental/financial-report/balance-sheet/hk",
|
|
488
539
|
kind: "json",
|
|
489
540
|
description: "Query HK balance sheet (China GAAP)",
|
|
541
|
+
expects: "list",
|
|
490
542
|
},
|
|
491
543
|
"fundamental.cash-flow-hk": {
|
|
492
544
|
method: "POST",
|
|
493
545
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/hk",
|
|
494
546
|
kind: "json",
|
|
495
547
|
description: "Query HK cash flow statement (China GAAP)",
|
|
548
|
+
expects: "list",
|
|
496
549
|
},
|
|
497
550
|
"fundamental.income-statement-us": {
|
|
498
551
|
method: "POST",
|
|
499
552
|
path: "/application/open-fundamental/financial-report/income-statement/us",
|
|
500
553
|
kind: "json",
|
|
501
554
|
description: "Query US income statement",
|
|
555
|
+
expects: "list",
|
|
502
556
|
},
|
|
503
557
|
"fundamental.balance-sheet-us": {
|
|
504
558
|
method: "POST",
|
|
505
559
|
path: "/application/open-fundamental/financial-report/balance-sheet/us",
|
|
506
560
|
kind: "json",
|
|
507
561
|
description: "Query US balance sheet",
|
|
562
|
+
expects: "list",
|
|
508
563
|
},
|
|
509
564
|
"fundamental.cash-flow-us": {
|
|
510
565
|
method: "POST",
|
|
511
566
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/us",
|
|
512
567
|
kind: "json",
|
|
513
568
|
description: "Query US cash flow statement",
|
|
569
|
+
expects: "list",
|
|
514
570
|
},
|
|
515
571
|
"fundamental.main-business": {
|
|
516
572
|
method: "POST",
|
|
517
573
|
path: "/application/open-fundamental/main-business",
|
|
518
574
|
kind: "json",
|
|
519
575
|
description: "Query main business composition",
|
|
576
|
+
expects: "list",
|
|
520
577
|
},
|
|
521
578
|
"fundamental.valuation-analysis": {
|
|
522
579
|
method: "POST",
|
|
@@ -530,6 +587,7 @@ const ENDPOINT_DEFS = {
|
|
|
530
587
|
path: "/application/open-fundamental/capital-structure/top-holders",
|
|
531
588
|
kind: "json",
|
|
532
589
|
description: "Query top holders (top10 / top10 float)",
|
|
590
|
+
expects: "list",
|
|
533
591
|
},
|
|
534
592
|
"fundamental.earning-forecast": {
|
|
535
593
|
method: "POST",
|
|
@@ -621,7 +679,7 @@ const ENDPOINT_DEFS = {
|
|
|
621
679
|
path: "/application/open-fundamental/bond/rating-change",
|
|
622
680
|
kind: "json",
|
|
623
681
|
description: "Query bond rating change history (max 10 bonds per call)",
|
|
624
|
-
billing: { per: "security", price: 0.4, maxUnits: 10 },
|
|
682
|
+
billing: { per: "security", price: 0.4, maxUnits: 10, unitsFrom: ["securityList"] },
|
|
625
683
|
retry: "no-replay",
|
|
626
684
|
expects: "list",
|
|
627
685
|
},
|
|
@@ -630,7 +688,7 @@ const ENDPOINT_DEFS = {
|
|
|
630
688
|
path: "/application/open-fundamental/bond/issuer-rating-change",
|
|
631
689
|
kind: "json",
|
|
632
690
|
description: "Query issuer rating change history (by bond code or issuer name)",
|
|
633
|
-
billing: { per: "issuer", price: 0.4, maxUnits: 10 },
|
|
691
|
+
billing: { per: "issuer", price: 0.4, maxUnits: 10, unitsFrom: ["securityList", "issuerNameList"] },
|
|
634
692
|
retry: "no-replay",
|
|
635
693
|
expects: "list",
|
|
636
694
|
},
|
|
@@ -828,7 +886,7 @@ const ENDPOINT_DEFS = {
|
|
|
828
886
|
path: "/application/open-ai/stock-summary/getList",
|
|
829
887
|
kind: "json",
|
|
830
888
|
description: "Stock highlights (refined research summary per security)",
|
|
831
|
-
billing: { per: "row", price: 1, maxUnits: 6000 },
|
|
889
|
+
billing: { per: "row", price: 1, maxUnits: 6000, unitsFrom: ["securityList"] },
|
|
832
890
|
retry: "no-replay",
|
|
833
891
|
timeoutMs: 120_000,
|
|
834
892
|
},
|
|
@@ -922,6 +980,18 @@ const ENDPOINT_DEFS = {
|
|
|
922
980
|
pagination: { enabled: true, maxPageSize: 20 },
|
|
923
981
|
retry: "no-replay",
|
|
924
982
|
},
|
|
983
|
+
"ai.expert-intel.list": {
|
|
984
|
+
method: "POST",
|
|
985
|
+
path: "/application/open-insight/expertIntel/getList",
|
|
986
|
+
kind: "json",
|
|
987
|
+
description: "List AI expert intel (takeaways distilled from expert meetings and minutes)",
|
|
988
|
+
billing: { per: "row", price: 20 },
|
|
989
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
990
|
+
rowId: "intelId",
|
|
991
|
+
rowIdUnverified: true,
|
|
992
|
+
retry: "no-replay",
|
|
993
|
+
expects: "list",
|
|
994
|
+
},
|
|
925
995
|
"ai.management-discuss-announcement": {
|
|
926
996
|
method: "POST",
|
|
927
997
|
path: "/application/open-ai/management-discuss/from-announcement",
|
|
@@ -1012,6 +1082,7 @@ const ENDPOINT_DEFS = {
|
|
|
1012
1082
|
path: "/application/open-vault/drive/getFolderList",
|
|
1013
1083
|
kind: "json",
|
|
1014
1084
|
description: "List the direct subfolders and files of a drive folder",
|
|
1085
|
+
expects: "object",
|
|
1015
1086
|
},
|
|
1016
1087
|
"vault.drive.create-folder": {
|
|
1017
1088
|
method: "POST",
|
|
@@ -1103,6 +1174,13 @@ const ENDPOINT_DEFS = {
|
|
|
1103
1174
|
pagination: { enabled: true, maxPageSize: 50, maxWindow: 10000 },
|
|
1104
1175
|
rowId: "msgId",
|
|
1105
1176
|
},
|
|
1177
|
+
"vault.wechat-message.download": {
|
|
1178
|
+
method: "GET",
|
|
1179
|
+
path: "/application/open-vault/wechatgroupmsg/download/file",
|
|
1180
|
+
kind: "download",
|
|
1181
|
+
description: "Download the file of a WeChat group message (document / image / audio / video)",
|
|
1182
|
+
fileOnSuccess: true,
|
|
1183
|
+
},
|
|
1106
1184
|
"vault.wechat-chatroom.list": {
|
|
1107
1185
|
method: "POST",
|
|
1108
1186
|
path: "/application/open-vault/wechatgroupmsg/chatroomId",
|
|
@@ -1122,6 +1200,7 @@ const ENDPOINT_DEFS = {
|
|
|
1122
1200
|
path: "/application/open-vault/stock-pool/getStockList",
|
|
1123
1201
|
kind: "json",
|
|
1124
1202
|
description: "List securities in stock pool(s)",
|
|
1203
|
+
expects: "array",
|
|
1125
1204
|
},
|
|
1126
1205
|
"vault.stock-pool.create": {
|
|
1127
1206
|
method: "POST",
|
|
@@ -1192,13 +1271,14 @@ const ENDPOINT_DEFS = {
|
|
|
1192
1271
|
path: "/application/open-alternative/EDB/search",
|
|
1193
1272
|
kind: "json",
|
|
1194
1273
|
description: "Search industry indicator list by keyword",
|
|
1274
|
+
expects: "array",
|
|
1195
1275
|
},
|
|
1196
1276
|
"alternative.edb-data": {
|
|
1197
1277
|
method: "POST",
|
|
1198
1278
|
path: "/application/open-alternative/EDB/getData",
|
|
1199
1279
|
kind: "json",
|
|
1200
1280
|
description: "Get industry indicator time-series data by indicator ID list",
|
|
1201
|
-
billing: { per: "indicator", price: 30, maxUnits: 10 },
|
|
1281
|
+
billing: { per: "indicator", price: 30, maxUnits: 10, unitsFrom: ["indicatorIdList"] },
|
|
1202
1282
|
},
|
|
1203
1283
|
"alternative.concept-info": {
|
|
1204
1284
|
method: "POST",
|
|
@@ -1238,6 +1318,7 @@ const ENDPOINT_DEFS = {
|
|
|
1238
1318
|
kind: "json",
|
|
1239
1319
|
description: "Search data indicators by keyword (returns indicatorCode + params)",
|
|
1240
1320
|
retry: "no-999999",
|
|
1321
|
+
expects: "array",
|
|
1241
1322
|
},
|
|
1242
1323
|
"indicator.cross-section": {
|
|
1243
1324
|
method: "POST",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -55,9 +55,9 @@ const ERROR_HINTS = {
|
|
|
55
55
|
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
56
56
|
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
57
57
|
"130002": "确认 ID 有效且本账号可见(云盘的文件 / 文件夹 ID 用 `vault drive-folder-list` 核对,笔记 ID 用 `vault stock-note-list` 核对;删除类命令收到此码可能是目标已被删掉);下载类还需检查 --file-type 取值是否合法。",
|
|
58
|
-
"130003": "资源未生成或该条记录未附带文件——换一条有附件的记录(列表里 hasAttachment/hasFile 为 true
|
|
58
|
+
"130003": "资源未生成或该条记录未附带文件——换一条有附件的记录(列表里 hasAttachment/hasFile 为 true 的),或稍后再试;`vault wechat-message-download` 收到此码是消息不存在、本账号无权访问或文件已不可用,重试不会变。",
|
|
59
59
|
"130004": "下载 ID 需为数字,检查该命令的 --*-id 参数是否传对。",
|
|
60
|
-
"130005": "对照命令 --help 检查 --file-type / --content-type
|
|
60
|
+
"130005": "对照命令 --help 检查 --file-type / --content-type 取值;`vault wechat-message-download` 收到此码是该消息为 text / url、没有文件——只有 document / image / audio / video 消息可下载。",
|
|
61
61
|
"140001": "稍后用对应 *-check 命令查询。",
|
|
62
62
|
"140002": "终态失败,立即重试同一请求不会变。msg 指向参数(EDE 的指标参数名/枚举以 indicator search 的 parameterList 为准)就改参数重新提交;msg 是「业务处理失败」则与参数无关,稍后再试,持续出现请带 trace 报障。",
|
|
63
63
|
"210001": "换一篇,或改用 list 取正文摘要。",
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { beijingDateString } from "./args.js";
|
|
2
2
|
import { normalizeRows } from "./normalize.js";
|
|
3
3
|
import { PAGE_CONCURRENCY, runWithConcurrency } from "./transport.js";
|
|
4
|
+
function weekdayBefore(day) {
|
|
5
|
+
const d = new Date(`${day}T00:00:00Z`);
|
|
6
|
+
do
|
|
7
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
8
|
+
while (d.getUTCDay() === 0 || d.getUTCDay() === 6);
|
|
9
|
+
return d.toISOString().slice(0, 10);
|
|
10
|
+
}
|
|
4
11
|
const PRICE_TOLERANCE = 0.0005;
|
|
5
12
|
const agrees = (close, price) => price > 0 && Math.abs(close - price) / price <= PRICE_TOLERANCE;
|
|
13
|
+
const CHANGE_TOLERANCE = 0.005;
|
|
6
14
|
const QUOTABLE = /^(\d{6}\.(SH|SZ|BJ)|\d{5}\.HK|[A-Z][A-Z0-9.]*\.(O|N|A))$/;
|
|
7
15
|
const QUOTE_BATCH = 500;
|
|
8
16
|
const CLOSES_WINDOW_DAYS = 10;
|
|
@@ -137,7 +145,8 @@ export async function fillComponentCodes(rows, lookup) {
|
|
|
137
145
|
}
|
|
138
146
|
});
|
|
139
147
|
const quotable = [...new Set([...candidates.values()].flat().filter((c) => ["stock", "dr"].includes(c.category) && QUOTABLE.test(c.code)).map((c) => c.code))];
|
|
140
|
-
const
|
|
148
|
+
const quotes = new Map((await runWithConcurrency(inBatches(quotable, QUOTE_BATCH), PAGE_CONCURRENCY, (batch) => splitting(batch, (codes) => lookup.quotes(codes)))).flatMap((quoted) => [...quoted]));
|
|
149
|
+
const prices = new Map([...quotes].map(([code, q]) => [code, [q.previous, q.latest].filter((p) => typeof p === "number")]));
|
|
141
150
|
const isQuotable = (c) => ["stock", "dr"].includes(c.category) && QUOTABLE.test(c.code);
|
|
142
151
|
const candidatesOf = (row) => candidates.get(row.componentName) ?? [];
|
|
143
152
|
const marketsOf = (listed) => new Set(listed.map((c) => market(c.code)));
|
|
@@ -184,7 +193,30 @@ export async function fillComponentCodes(rows, lookup) {
|
|
|
184
193
|
history.set(code, series);
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
|
-
const
|
|
196
|
+
const unshown = (close, c) => {
|
|
197
|
+
const { previous, latest, date } = quotes.get(c.code) ?? {};
|
|
198
|
+
const onLatest = latest !== undefined && agrees(close, latest);
|
|
199
|
+
const onPrevious = previous !== undefined && agrees(close, previous);
|
|
200
|
+
if (!onLatest && !onPrevious)
|
|
201
|
+
return false;
|
|
202
|
+
if (date === undefined)
|
|
203
|
+
return true;
|
|
204
|
+
const days = (history.get(c.code) ?? []).map(([day]) => day);
|
|
205
|
+
const reachesQuote = days.some((day) => day >= date);
|
|
206
|
+
const reachesPrevious = reachesQuote || days.includes(weekdayBefore(date));
|
|
207
|
+
return (onLatest && !reachesQuote) || (onPrevious && !reachesPrevious);
|
|
208
|
+
};
|
|
209
|
+
const marketDays = new Map([...etfs].map(([fundCode, { own }]) => [fundCode, listDates(own, candidates, history)]));
|
|
210
|
+
const startsLate = (row, c) => {
|
|
211
|
+
const first = history.get(c.code)?.[0]?.[0];
|
|
212
|
+
if (first === undefined)
|
|
213
|
+
return true;
|
|
214
|
+
const day = marketDays.get(row.fundCode)?.get(market(c.code));
|
|
215
|
+
if (day !== undefined)
|
|
216
|
+
return first > day;
|
|
217
|
+
return candidatesOf(row).some((o) => o.code !== c.code && (history.get(o.code) ?? []).some(([day, price]) => day < first && agrees(row.close, price)));
|
|
218
|
+
};
|
|
219
|
+
const mayBe = (row, c) => !history.has(c.code) || history.get(c.code).some(([, close]) => agrees(row.close, close)) || unshown(row.close, c) || startsLate(row, c);
|
|
188
220
|
for (const { own, etf } of etfs.values()) {
|
|
189
221
|
for (const row of own) {
|
|
190
222
|
const matched = unverified.get(row);
|
|
@@ -210,11 +242,35 @@ export async function fillComponentCodes(rows, lookup) {
|
|
|
210
242
|
}
|
|
211
243
|
}
|
|
212
244
|
}
|
|
245
|
+
for (const { own, etf } of etfs.values()) {
|
|
246
|
+
for (const row of own) {
|
|
247
|
+
const change = row.pctChange;
|
|
248
|
+
if (codes.has(row) || typeof change !== "number" || typeof row.close !== "number")
|
|
249
|
+
continue;
|
|
250
|
+
const close = row.close;
|
|
251
|
+
const pool = candidatesOf(row).filter((c) => !etf.used.has(c.code));
|
|
252
|
+
if (!pool.length || !pool.every((c) => isQuotable(c) && history.has(c.code)))
|
|
253
|
+
continue;
|
|
254
|
+
const atClose = (c) => history.get(c.code).filter(([, price]) => agrees(close, price));
|
|
255
|
+
const fits = (c) => atClose(c).filter(([, , moved]) => moved !== undefined && Math.abs(moved - change) <= CHANGE_TOLERANCE);
|
|
256
|
+
const hit = pool.filter((c) => fits(c).length);
|
|
257
|
+
if (!hit.length)
|
|
258
|
+
continue;
|
|
259
|
+
const unknown = pool.some((c) => !hit.includes(c) && (atClose(c).some(([, , moved]) => moved === undefined)
|
|
260
|
+
|| unshown(close, c)
|
|
261
|
+
|| startsLate(row, c)));
|
|
262
|
+
if (unknown || marketsOf(hit).size > 1)
|
|
263
|
+
continue;
|
|
264
|
+
const code = narrow(hit, etf);
|
|
265
|
+
if (code)
|
|
266
|
+
place(row, code, etf);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
213
269
|
for (const { own, etf } of etfs.values()) {
|
|
214
270
|
const markets = new Set([...etf.used].map(market));
|
|
215
271
|
let undecided = own.some((r) => failed.has(r.componentName));
|
|
216
272
|
for (const r of own) {
|
|
217
|
-
if (codes.get(r)
|
|
273
|
+
if (codes.get(r))
|
|
218
274
|
continue;
|
|
219
275
|
const held = marketsOf(candidatesOf(r).filter((c) => !etf.used.has(c.code) && (typeof r.close !== "number" || mayBe(r, c))));
|
|
220
276
|
if (held.size > 1)
|
|
@@ -329,14 +385,14 @@ export function clientLookup(client) {
|
|
|
329
385
|
return Array.isArray(data?.list) ? data.list : [];
|
|
330
386
|
},
|
|
331
387
|
async closes(codes, startDate, endDate) {
|
|
332
|
-
const data = normalizeRows(await client.call("quote.day-kline", { securityList: codes, startDate, endDate, fieldList: ["securityCode", "tradeDate", "close"], limit: 10_000 }));
|
|
388
|
+
const data = normalizeRows(await client.call("quote.day-kline", { securityList: codes, startDate, endDate, fieldList: ["securityCode", "tradeDate", "close", "preClose"], limit: 10_000 }));
|
|
333
389
|
const list = Array.isArray(data) ? data : (data?.list ?? []);
|
|
334
390
|
const out = new Map();
|
|
335
391
|
for (const k of list) {
|
|
336
392
|
if (typeof k.securityCode !== "string" || typeof k.tradeDate !== "string" || typeof k.close !== "number")
|
|
337
393
|
continue;
|
|
338
394
|
const series = out.get(k.securityCode) ?? [];
|
|
339
|
-
series.push([k.tradeDate, k.close]);
|
|
395
|
+
series.push(typeof k.preClose === "number" && k.preClose > 0 ? [k.tradeDate, k.close, (k.close / k.preClose - 1) * 100] : [k.tradeDate, k.close]);
|
|
340
396
|
out.set(k.securityCode, series);
|
|
341
397
|
}
|
|
342
398
|
for (const series of out.values())
|
|
@@ -347,10 +403,11 @@ export function clientLookup(client) {
|
|
|
347
403
|
const data = normalizeRows(await client.call("quote.realtime", { securityList: codes }));
|
|
348
404
|
const list = Array.isArray(data) ? data : (data?.list ?? []);
|
|
349
405
|
const out = new Map();
|
|
406
|
+
const price = (value) => (typeof value === "number" ? value : undefined);
|
|
350
407
|
for (const q of list) {
|
|
351
|
-
const
|
|
352
|
-
if (typeof q.securityCode === "string" &&
|
|
353
|
-
out.set(q.securityCode,
|
|
408
|
+
const quote = { previous: price(q.preClose), latest: price(q.latestPrice), date: typeof q.tradeDate === "string" && /^\d{4}-\d{2}-\d{2}$/.test(q.tradeDate) ? q.tradeDate : undefined };
|
|
409
|
+
if (typeof q.securityCode === "string" && (quote.previous !== undefined || quote.latest !== undefined))
|
|
410
|
+
out.set(q.securityCode, quote);
|
|
354
411
|
}
|
|
355
412
|
return out;
|
|
356
413
|
},
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export const EXIT_FAILED = 1;
|
|
2
|
+
export const EXIT_REFUSED = 2;
|
|
2
3
|
export const EXIT_INCOMPLETE = 3;
|
|
3
4
|
export const EXIT_SUPERSEDED = 4;
|
|
4
5
|
export function markFailed() {
|
|
5
6
|
process.exitCode = EXIT_FAILED;
|
|
6
7
|
}
|
|
8
|
+
export function markRefused() {
|
|
9
|
+
process.exitCode = EXIT_REFUSED;
|
|
10
|
+
}
|
|
7
11
|
export function markIncomplete() {
|
|
8
12
|
if (process.exitCode !== EXIT_FAILED)
|
|
9
13
|
process.exitCode = EXIT_INCOMPLETE;
|
|
@@ -3,11 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import { isAsyncPending, pollUntilDone } from "./asyncContent.js";
|
|
4
4
|
import { saveDownloadResult } from "./download.js";
|
|
5
5
|
import { ApiError, ValidationError } from "./errors.js";
|
|
6
|
+
import { assertNotStatePath } from "./statePaths.js";
|
|
6
7
|
export const FILE_PARSE_MAX_BYTES = 100 * 1024 * 1024;
|
|
7
8
|
export async function submitFileParse(client, filePath) {
|
|
8
|
-
const
|
|
9
|
+
const shownAs = path.resolve(filePath);
|
|
10
|
+
let resolved;
|
|
9
11
|
let stat;
|
|
10
12
|
try {
|
|
13
|
+
resolved = await fs.realpath(filePath);
|
|
11
14
|
stat = await fs.stat(resolved);
|
|
12
15
|
}
|
|
13
16
|
catch {
|
|
@@ -15,17 +18,21 @@ export async function submitFileParse(client, filePath) {
|
|
|
15
18
|
}
|
|
16
19
|
if (!stat.isFile())
|
|
17
20
|
throw new ValidationError(`Not a file: ${filePath}`);
|
|
21
|
+
await assertNotStatePath(resolved, "--file");
|
|
18
22
|
if (stat.size === 0)
|
|
19
23
|
throw new ValidationError(`File is empty: ${filePath}`);
|
|
20
24
|
if (stat.size > FILE_PARSE_MAX_BYTES) {
|
|
21
25
|
throw new ValidationError(`File is ${(stat.size / 1024 / 1024).toFixed(1)}MB — the parse API accepts at most 100MB`);
|
|
22
26
|
}
|
|
23
|
-
if (path.extname(
|
|
27
|
+
if (path.extname(shownAs).toLowerCase() !== ".pdf") {
|
|
24
28
|
throw new ValidationError(`Only PDF files are supported: ${filePath}`);
|
|
25
29
|
}
|
|
26
30
|
const data = await fs.readFile(resolved);
|
|
31
|
+
if (!data.subarray(0, 1024).toString("latin1").includes("%PDF-")) {
|
|
32
|
+
throw new ValidationError(`Not a PDF (no %PDF- header in its first 1KB): ${filePath}`);
|
|
33
|
+
}
|
|
27
34
|
const result = await client.uploadFile("tool.file-parse.submit", {
|
|
28
|
-
filename: path.basename(
|
|
35
|
+
filename: path.basename(shownAs),
|
|
29
36
|
data,
|
|
30
37
|
contentType: "application/pdf",
|
|
31
38
|
});
|
package/dist/src/core/printer.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getRowSink } from "./rowSink.js";
|
|
|
6
6
|
import { extractTitles, writeTitleCache } from "./titleCache.js";
|
|
7
7
|
import { CLI_VERSION } from "../version.js";
|
|
8
8
|
import { decidedExitCode, EXIT_SUPERSEDED, markIncomplete, markSuperseded } from "./exitStatus.js";
|
|
9
|
+
import { describeNoRequest, isDryRun } from "./dryRun.js";
|
|
9
10
|
const LARGE_RESULT_ROWS = 50_000;
|
|
10
11
|
export const SUPERSEDED_EXIT = EXIT_SUPERSEDED;
|
|
11
12
|
export async function warnIfSuperseded(output, digest) {
|
|
@@ -101,19 +102,50 @@ async function stageExportMeta(output, format, rows, columns, normalized, comple
|
|
|
101
102
|
},
|
|
102
103
|
};
|
|
103
104
|
}
|
|
105
|
+
let selectedColumns;
|
|
106
|
+
export function setColumns(names) {
|
|
107
|
+
selectedColumns = names.length > 0 ? names : undefined;
|
|
108
|
+
}
|
|
109
|
+
export function columnsSelected() {
|
|
110
|
+
return selectedColumns !== undefined;
|
|
111
|
+
}
|
|
112
|
+
function projectColumns(value) {
|
|
113
|
+
const names = selectedColumns;
|
|
114
|
+
if (!names)
|
|
115
|
+
return value;
|
|
116
|
+
const isRecord = (row) => Boolean(row) && typeof row === "object" && !Array.isArray(row);
|
|
117
|
+
const rows = pickList(value) ?? (isRecord(value) ? [value] : []);
|
|
118
|
+
const present = new Set(rows.filter(isRecord).flatMap((row) => Object.keys(row)));
|
|
119
|
+
const absent = names.filter((name) => !present.has(name));
|
|
120
|
+
if (absent.length > 0 && present.size > 0) {
|
|
121
|
+
process.stderr.write(`[gangtise] warning: --columns names ${absent.join(", ")}, which no row has; the rows have ${[...present].join(", ")}.\n`);
|
|
122
|
+
}
|
|
123
|
+
const pick = (row) => (isRecord(row) ? Object.fromEntries(names.map((name) => [name, row[name] ?? null])) : row);
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value.map(pick);
|
|
126
|
+
if (!isRecord(value))
|
|
127
|
+
return value;
|
|
128
|
+
return Array.isArray(value.list) ? { ...value, list: value.list.map(pick) } : pick(value);
|
|
129
|
+
}
|
|
104
130
|
export async function printData(data, format, output, cache) {
|
|
131
|
+
if (isDryRun()) {
|
|
132
|
+
process.stdout.write(`${JSON.stringify(describeNoRequest(output), null, 2)}\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
105
135
|
const sink = getRowSink(data);
|
|
106
136
|
if (sink && !sink.opened)
|
|
107
137
|
data.list = sink.takeBuffer();
|
|
108
138
|
const streamed = sink?.opened ? sink : undefined;
|
|
109
|
-
const columns = data && typeof data === "object" && Array.isArray(data.fieldList)
|
|
139
|
+
const columns = selectedColumns ?? (data && typeof data === "object" && Array.isArray(data.fieldList)
|
|
110
140
|
? data.fieldList
|
|
111
|
-
: undefined;
|
|
112
|
-
const
|
|
141
|
+
: undefined);
|
|
142
|
+
const whole = normalizeRows(data);
|
|
143
|
+
const wholeItems = pickList(whole);
|
|
144
|
+
const normalized = projectColumns(whole);
|
|
113
145
|
const items = pickList(normalized);
|
|
114
146
|
const showing = streamed ? streamed.rows : (items?.length ?? 0);
|
|
115
147
|
if (cache) {
|
|
116
|
-
const titles = { ...(streamed?.titles ?? {}), ...(
|
|
148
|
+
const titles = { ...(streamed?.titles ?? {}), ...(wholeItems ? extractTitles(wholeItems, cache) : {}) };
|
|
117
149
|
if (Object.keys(titles).length > 0) {
|
|
118
150
|
writeTitleCache(cache.endpointKey, titles).catch(() => { });
|
|
119
151
|
}
|
|
@@ -123,13 +155,13 @@ export async function printData(data, format, output, cache) {
|
|
|
123
155
|
if (typeof meta.total === "number" && format !== "json") {
|
|
124
156
|
process.stderr.write(`Total: ${meta.total}, showing: ${showing}\n`);
|
|
125
157
|
}
|
|
126
|
-
if (meta.partial === true)
|
|
127
|
-
markIncomplete();
|
|
128
158
|
}
|
|
159
|
+
if (whole && typeof whole === "object" && !Array.isArray(whole) && whole.partial === true)
|
|
160
|
+
markIncomplete();
|
|
129
161
|
const complete = decidedExitCode() === 0;
|
|
130
162
|
if (output) {
|
|
131
163
|
const staged = format === "csv" || format === "jsonl"
|
|
132
|
-
? await stageExportMeta(output, format, streamed ? streamed.dataRows : countOutputRows(normalized, format), columns,
|
|
164
|
+
? await stageExportMeta(output, format, streamed ? streamed.dataRows : countOutputRows(normalized, format), columns, whole, complete)
|
|
133
165
|
: null;
|
|
134
166
|
let digest;
|
|
135
167
|
try {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_TOKEN_CACHE_PATH, loadConfig } from "./config.js";
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
const identity = async (target) => {
|
|
6
|
+
const stat = await fs.stat(target).catch(() => undefined);
|
|
7
|
+
return stat ? `${stat.dev}:${stat.ino}` : undefined;
|
|
8
|
+
};
|
|
9
|
+
async function physical(target) {
|
|
10
|
+
const rest = [];
|
|
11
|
+
let head = path.isAbsolute(target) ? target : `${process.cwd()}${path.sep}${target}`;
|
|
12
|
+
for (;;) {
|
|
13
|
+
const real = await fs.realpath(head).catch(() => undefined);
|
|
14
|
+
if (real)
|
|
15
|
+
return path.join(real, ...rest);
|
|
16
|
+
const parent = path.dirname(head);
|
|
17
|
+
if (parent === head)
|
|
18
|
+
return path.resolve(target);
|
|
19
|
+
rest.unshift(path.basename(head));
|
|
20
|
+
head = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function assertNotStatePath(target, option) {
|
|
24
|
+
const stateDir = path.dirname(DEFAULT_TOKEN_CACHE_PATH);
|
|
25
|
+
const tokenCache = loadConfig().tokenCachePath;
|
|
26
|
+
const guarded = new Set((await Promise.all([stateDir, tokenCache].map(identity))).filter((id) => id !== undefined));
|
|
27
|
+
const resolved = await physical(target);
|
|
28
|
+
let refused = resolved === await physical(tokenCache);
|
|
29
|
+
for (let current = resolved; !refused;) {
|
|
30
|
+
const id = await identity(current);
|
|
31
|
+
if (id && guarded.has(id))
|
|
32
|
+
refused = true;
|
|
33
|
+
const parent = path.dirname(current);
|
|
34
|
+
if (parent === current)
|
|
35
|
+
break;
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
if (refused) {
|
|
39
|
+
throw new ValidationError(`${option} ${target} points at gangtise's own state (${stateDir}, or the token cache ${tokenCache}), which holds the access token: refused. Pass a path outside it.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -34,6 +34,13 @@ function retryAfterFromError(error) {
|
|
|
34
34
|
return undefined;
|
|
35
35
|
}
|
|
36
36
|
let cachedDispatcher = null;
|
|
37
|
+
let requestSent = false;
|
|
38
|
+
export function noteRequestSent() {
|
|
39
|
+
requestSent = true;
|
|
40
|
+
}
|
|
41
|
+
export function anyRequestSent() {
|
|
42
|
+
return requestSent;
|
|
43
|
+
}
|
|
37
44
|
const requireLazily = createRequire(import.meta.url);
|
|
38
45
|
export function getDispatcher() {
|
|
39
46
|
if (!cachedDispatcher) {
|