daxiapi-cli 2.3.3 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -220,6 +220,19 @@ daxiapi report finance 600036
220
220
  daxiapi report finance 000001
221
221
  ```
222
222
 
223
+ ### 新闻数据
224
+
225
+ ```bash
226
+ # 获取个股舆情(内部自动将 code 转为 secid)
227
+ daxiapi news sentiment -c 600031 -p 20
228
+
229
+ # 获取个股公告
230
+ daxiapi news notice -c 600031 -p 20 -i 1
231
+
232
+ # 获取个股研报(支持时间区间)
233
+ daxiapi news report -c 600031 -p 25 -i 1 -b 2026-01-01 -e 2026-04-08
234
+ ```
235
+
223
236
  ### 工具
224
237
 
225
238
  ```bash
package/bin/index.js CHANGED
@@ -21,5 +21,6 @@ require('../commands/dividend')(program);
21
21
  require('../commands/hotrank')(program);
22
22
  require('../commands/turnover')(program);
23
23
  require('../commands/report')(program);
24
+ require('../commands/news')(program);
24
25
 
25
26
  program.parse(process.argv);
@@ -0,0 +1,96 @@
1
+ const api = require('../lib/api');
2
+ const {handleError, createParameterError} = require('../lib/error');
3
+ const {encode} = require('../lib/output');
4
+ const {getSecid} = require('../lib/utils');
5
+
6
+ function toPositiveInt(value, fieldName, examples) {
7
+ const number = Number.parseInt(value, 10);
8
+ if (!Number.isInteger(number) || number <= 0) {
9
+ throw createParameterError('参数无效', [`参数 '${fieldName}' 必须是大于0的整数`], examples);
10
+ }
11
+ return number;
12
+ }
13
+
14
+ function validateDate(value, fieldName, examples) {
15
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
16
+ throw createParameterError('参数无效', [`参数 '${fieldName}' 必须是 YYYY-MM-DD 格式`], examples);
17
+ }
18
+ }
19
+
20
+ module.exports = function (program) {
21
+ const newsCmd = program.command('news').description('获取个股舆情、公告、研报数据,用于跟踪个股信息面变化。');
22
+
23
+ newsCmd
24
+ .command('sentiment')
25
+ .description('获取个股舆情列表。')
26
+ .option('-c, --code <code>', '股票代码(6位),如600031', '600031')
27
+ .option('-p, --page-size <pageSize>', '每页条数,默认20', '20')
28
+ .action(async options => {
29
+ try {
30
+ if (!options.code) {
31
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], ['daxiapi news sentiment -c600031 -p20']);
32
+ }
33
+
34
+ const secid = getSecid(options.code);
35
+ const pageSize = toPositiveInt(options.pageSize, 'pageSize', ['daxiapi news sentiment -c600031 -p20']);
36
+ const data = await api.getNewsSentiment(secid, pageSize);
37
+ console.log('```toon\n' + encode(data) + '\n```');
38
+ } catch (error) {
39
+ handleError(error);
40
+ process.exit(1);
41
+ }
42
+ });
43
+
44
+ newsCmd
45
+ .command('notice')
46
+ .description('获取个股公告列表。')
47
+ .option('-c, --code <code>', '股票代码(6位),如600031', '600031')
48
+ .option('-p, --page-size <pageSize>', '每页条数,默认20', '20')
49
+ .option('-i, --page-index <pageIndex>', '页码,默认1', '1')
50
+ .action(async options => {
51
+ try {
52
+ if (!options.code) {
53
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], ['daxiapi news notice -c600031 -p20 -i1']);
54
+ }
55
+
56
+ const pageSize = toPositiveInt(options.pageSize, 'pageSize', ['daxiapi news notice -c600031 -p20 -i1']);
57
+ const pageIndex = toPositiveInt(options.pageIndex, 'pageIndex', ['daxiapi news notice -c600031 -p20 -i1']);
58
+ const data = await api.getNewsNotice(options.code, pageSize, pageIndex);
59
+ console.log('```toon\n' + encode(data) + '\n```');
60
+ } catch (error) {
61
+ handleError(error);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ newsCmd
67
+ .command('report')
68
+ .description('获取个股研报列表。')
69
+ .option('-c, --code <code>', '股票代码(6位),如600031', '600031')
70
+ .option('-p, --page-size <pageSize>', '每页条数,默认25', '25')
71
+ .option('-i, --page-index <pageIndex>', '页码,默认1', '1')
72
+ .option('-b, --begin-time <beginTime>', '开始日期,格式 YYYY-MM-DD', '2026-01-01')
73
+ .option('-e, --end-time <endTime>', '结束日期,格式 YYYY-MM-DD')
74
+ .action(async options => {
75
+ try {
76
+ if (!options.code) {
77
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], [
78
+ 'daxiapi news report -c600031 -p25 -i1 -b2026-01-01 -e2026-04-08'
79
+ ]);
80
+ }
81
+
82
+ validateDate(options.beginTime, 'beginTime', ['daxiapi news report -b2026-01-01']);
83
+ const endTime = options.endTime || new Date().toISOString().slice(0, 10);
84
+ validateDate(endTime, 'endTime', ['daxiapi news report -e2026-04-08']);
85
+
86
+ const pageSize = toPositiveInt(options.pageSize, 'pageSize', ['daxiapi news report -p25']);
87
+ const pageIndex = toPositiveInt(options.pageIndex, 'pageIndex', ['daxiapi news report -i1']);
88
+
89
+ const data = await api.getNewsReport(options.code, pageSize, pageIndex, options.beginTime, endTime);
90
+ console.log('```toon\n' + encode(data) + '\n```');
91
+ } catch (error) {
92
+ handleError(error);
93
+ process.exit(1);
94
+ }
95
+ });
96
+ };
package/commands/stock.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const config = require('../lib/config');
2
2
  const api = require('../lib/api');
3
3
  const {handleError, createParameterError} = require('../lib/error');
4
- const {output} = require('../lib/output');
4
+ const {output, encode} = require('../lib/output');
5
5
 
6
6
  const SUPPORTED_PATTERNS = [
7
7
  'gxl',
@@ -142,4 +142,35 @@ module.exports = function (program) {
142
142
  process.exit(1);
143
143
  }
144
144
  });
145
+
146
+ stockCmd
147
+ .command('capital-flow <code>')
148
+ .description(
149
+ '获取个股主力资金流向数据,返回最近N天的资金动向。' +
150
+ '包括主力净流入、主力净流入占比、五日主力净流入、板块资金流向等详细数据。' +
151
+ '支持6位数字代码,自动识别沪深市场。可用于分析主力资金动向和资金面分析。'
152
+ )
153
+ .option('--days <days>', '返回天数(默认5天)', '5')
154
+ .action(async (code, options) => {
155
+ try {
156
+ if (!code) {
157
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], ['daxiapi stock capital-flow 000001']);
158
+ }
159
+
160
+ const days = parseInt(options.days, 10);
161
+ if (isNaN(days) || days < 1 || days > 30) {
162
+ throw createParameterError(
163
+ '参数无效',
164
+ ["参数 'days' 必须是1-30之间的整数"],
165
+ ['daxiapi stock capital-flow 000001 --days 5', 'daxiapi stock capital-flow 000001 --days 10']
166
+ );
167
+ }
168
+
169
+ const data = await api.getCapitalFlow(code, days);
170
+ console.log('```toon\n' + encode(data) + '\n```');
171
+ } catch (error) {
172
+ handleError(error);
173
+ process.exit(1);
174
+ }
175
+ });
145
176
  };
package/lib/api.js CHANGED
@@ -156,8 +156,6 @@ async function getStockRank(type = 'hour', listType = 'normal') {
156
156
  try {
157
157
  const params = {stock_type: 'a', list_type: listType};
158
158
 
159
- // 只有 normal 和 skyrocket 才有 type 参数,默认为 hour
160
- // 其他榜单类型固定为 day
161
159
  if (listType === 'normal' || listType === 'skyrocket') {
162
160
  params.type = type;
163
161
  } else {
@@ -204,19 +202,6 @@ async function getPlateRank(type = 'concept') {
204
202
  const data = extractData(response, 'plate_list');
205
203
 
206
204
  return data.map(d => {
207
- // - code: "881160"
208
- // rise_and_fall: 1.4869
209
- // etf_rise_and_fall: -0.1488
210
- // hot_rank_chg: 0
211
- // market_id: 48
212
- // hot_tag: 连续11天上榜
213
- // etf_product_id: "159766"
214
- // rate: "10366.5"
215
- // etf_name: 旅游ETF富国
216
- // name: 旅游及酒店
217
- // tag: 1家涨停
218
- // etf_market_id: 36
219
- // order: 20
220
205
  return {
221
206
  code: d.code,
222
207
  name: d.name,
@@ -270,19 +255,15 @@ async function getTurnoverData(type = 'day') {
270
255
  const currentTurnover = latest[1];
271
256
  const prevTurnover = previous[1];
272
257
  const diff = currentTurnover - prevTurnover;
273
- const currentYi = currentTurnover / 100000000; // 先转为亿
274
- const currentWanYi = (currentYi / 10000).toFixed(2); // 再转为万亿
275
- const diffYi = (Math.abs(diff) / 100000000).toFixed(2); // 转为亿
258
+ const currentYi = currentTurnover / 100000000;
259
+ const currentWanYi = (currentYi / 10000).toFixed(2);
260
+ const diffYi = (Math.abs(diff) / 100000000).toFixed(2);
276
261
  const formattedData = {
277
262
  当前成交额: currentWanYi + '万亿',
278
263
  变化量: (diff > 0 ? '增加' : '减少') + diffYi + '亿',
279
264
  较上日: diff > 0 ? '增加' : '减少'
280
265
  };
281
266
  const isTrading = minuteData.isTrading;
282
- // '0': { val: 1623545300000, name: '当日成交额', key: 'turnover' },
283
- // '1': { val: 1668872000000, name: '昨日成交额', key: 'turnover_pre' },
284
- // '2': { val: -45326700000, name: '较昨日变动', key: 'turnover_change' },
285
- // '3': { val: 1623545300000, name: '预测全天成交额', key: 'predict_turnover' },
286
267
  const rs = {};
287
268
  minuteData.header.forEach((item, index) => {
288
269
  rs[item.name] = item.val;
@@ -339,6 +320,124 @@ async function getTurnoverDataByMinute() {
339
320
  }
340
321
  }
341
322
 
323
+ function normalizeHttps(url) {
324
+ if (!url) {
325
+ return '';
326
+ }
327
+ return String(url).replace(/^http:\/\//, 'https://');
328
+ }
329
+
330
+ async function getNewsSentiment(secid, pageSize = 20) {
331
+ const response = await axios.get('https://np-listapi.eastmoney.com/comm/web/getListInfo', {
332
+ params: {
333
+ client: 'web',
334
+ biz: 'web_voice',
335
+ mTypeAndCode: secid,
336
+ pageSize,
337
+ type: 1,
338
+ req_trace: '3d4e684212bee1464c1b44611236955b',
339
+ },
340
+ });
341
+
342
+ const payload = response.data || {};
343
+ const list = payload.data?.list || [];
344
+ return {
345
+ pageIndex: payload.data?.page_index || 1,
346
+ pageSize: payload.data?.page_size || pageSize,
347
+ total: payload.data?.totle_hits || 0,
348
+ list: list.map((item) => ({
349
+ title: item.Art_Title,
350
+ showTime: item.Art_ShowTime,
351
+ artCode: item.Art_Code,
352
+ url: normalizeHttps(item.Art_Url),
353
+ originUrl: normalizeHttps(item.Art_OriginUrl),
354
+ })),
355
+ };
356
+ }
357
+
358
+ async function getNewsNotice(code, pageSize = 20, pageIndex = 1) {
359
+ const response = await axios.get('https://np-anotice-stock.eastmoney.com/api/security/ann', {
360
+ params: {
361
+ sr: -1,
362
+ page_size: pageSize,
363
+ page_index: pageIndex,
364
+ ann_type: 'A',
365
+ client_source: 'web',
366
+ stock_list: code,
367
+ f_node: 0,
368
+ s_node: 0,
369
+ },
370
+ });
371
+
372
+ const payload = response.data || {};
373
+ const data = payload.data || {};
374
+ const list = data.list || [];
375
+
376
+ return {
377
+ pageIndex: data.page_index || pageIndex,
378
+ pageSize: data.page_size || pageSize,
379
+ total: data.total_hits || 0,
380
+ list: list.map((item) => {
381
+ const stockCode = item.codes?.[0]?.stock_code || code;
382
+ return {
383
+ title: item.title,
384
+ noticeDate: item.notice_date,
385
+ displayTime: item.display_time,
386
+ artCode: item.art_code,
387
+ stockCode,
388
+ url: `https://data.eastmoney.com/notices/detail/${stockCode}/${item.art_code}.html`,
389
+ columns: (item.columns || []).map((column) => column.column_name),
390
+ };
391
+ }),
392
+ };
393
+ }
394
+
395
+ async function getNewsReport(code, pageSize = 25, pageIndex = 1, beginTime = '2026-01-01', endTime) {
396
+ const response = await axios.get('https://reportapi.eastmoney.com/report/list', {
397
+ params: {
398
+ pageNo: pageIndex,
399
+ pageSize,
400
+ code,
401
+ industryCode: '*',
402
+ industry: '*',
403
+ rating: '*',
404
+ ratingchange: '*',
405
+ beginTime,
406
+ endTime,
407
+ fields: '',
408
+ qType: 0,
409
+ sort: 'publishDate,desc',
410
+ },
411
+ });
412
+
413
+ const payload = response.data || {};
414
+ const list = payload.data || [];
415
+
416
+ return {
417
+ pageIndex: payload.pageNo || pageIndex,
418
+ pageSize: payload.size || pageSize,
419
+ total: payload.hits || 0,
420
+ list: list.map((item) => ({
421
+ title: item.title,
422
+ stockCode: item.stockCode,
423
+ stockName: item.stockName,
424
+ publishDate: item.publishDate,
425
+ orgName: item.orgName,
426
+ rating: item.emRatingName,
427
+ infoCode: item.infoCode,
428
+ predictNextTwoYearEps: item.predictNextTwoYearEps,
429
+ predictNextTwoYearPe: item.predictNextTwoYearPe,
430
+ predictNextYearEps: item.predictNextYearEps,
431
+ predictNextYearPe: item.predictNextYearPe,
432
+ predictThisYearEps: item.predictThisYearEps,
433
+ predictThisYearPe: item.predictThisYearPe,
434
+ predictLastYearEps: item.predictLastYearEps,
435
+ predictLastYearPe: item.predictLastYearPe,
436
+ url: `https://data.eastmoney.com/report/info/${item.infoCode}.html`,
437
+ })),
438
+ };
439
+ }
440
+
342
441
  module.exports = {
343
442
  getMarketData,
344
443
  getMarketTemp,
@@ -361,5 +460,8 @@ module.exports = {
361
460
  getPlateRank,
362
461
  getTurnoverData,
363
462
  getTurnoverDataByMinute,
364
- getFinanceReportDetail
463
+ getFinanceReportDetail,
464
+ getNewsSentiment,
465
+ getNewsNotice,
466
+ getNewsReport,
365
467
  };
package/lib/caibao.js CHANGED
@@ -2,59 +2,57 @@
2
2
  // const vm = require('vm');
3
3
 
4
4
  // ?ut=&invt=2&fltt=2&fields=&secid=1.600862&cb=&_=1601168994587
5
+ const axios = require('axios');
5
6
  const picker = require('@ksky521/html-picker');
6
7
  const iconv = require('./iconv');
7
8
 
8
9
  const {getHeader} = require('./thsUtils');
9
10
 
10
- const request = require('request-promise-native');
11
-
12
11
  const API_URL = 'http://basic.10jqka.com.cn/';
13
12
 
14
- function getDetail(stockId) {
13
+ async function getDetail(stockId) {
15
14
  const prefix = `${API_URL}${stockId}`;
16
15
  const url = `${prefix}/finance.html`;
17
- // console.log(url);
18
- return request({
19
- uri: url,
20
- encoding: null,
16
+ const response = await axios.get(url, {
17
+ responseType: 'arraybuffer',
18
+ timeout: 30000,
21
19
  headers: getHeader({Referrer: prefix})
22
- }).then(data => {
23
- const html = iconv(data);
24
- let rs = {};
25
- picker(html, {
26
- data: {
27
- selector: 'p#main',
28
- handler($node) {
29
- rs = JSON.parse($node.html());
30
- }
20
+ });
21
+
22
+ const html = iconv(response.data);
23
+ let rs = {};
24
+ picker(html, {
25
+ data: {
26
+ selector: 'p#main',
27
+ handler($node) {
28
+ rs = JSON.parse($node.html());
31
29
  }
32
- });
33
- const titles = rs.title;
34
- const content = rs.report;
35
- const reports = [];
36
- for (let index = 0; index < content[0].length; index++) {
37
- const report = {};
38
- for (let i = 0; i < titles.length; i++) {
39
- let title = titles[i];
40
- if (typeof title === 'string') {
41
- title = title.trim();
42
- } else if (Array.isArray(title)) {
43
- title = title[0];
44
- }
45
- if (content[index]) {
46
- report[title] = content[i][index];
47
- } else {
48
- // console.log(report[title]);
49
- }
30
+ }
31
+ });
32
+ const titles = rs.title;
33
+ const content = rs.report;
34
+ const reports = [];
35
+ for (let index = 0; index < content[0].length; index++) {
36
+ const report = {};
37
+ for (let i = 0; i < titles.length; i++) {
38
+ let title = titles[i];
39
+ if (typeof title === 'string') {
40
+ title = title.trim();
41
+ } else if (Array.isArray(title)) {
42
+ title = title[0];
50
43
  }
51
- if (Object.keys(report).length > 0) {
52
- reports.push(report);
44
+ if (content[index]) {
45
+ report[title] = content[i][index];
46
+ } else {
47
+ // console.log(report[title]);
53
48
  }
54
49
  }
55
- // console.log(titles.length, content.length);
56
- return reports;
57
- });
50
+ if (Object.keys(report).length > 0) {
51
+ reports.push(report);
52
+ }
53
+ }
54
+
55
+ return reports;
58
56
  }
59
57
 
60
58
  module.exports = getDetail;
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "daxiapi-cli",
3
- "version": "2.3.3",
3
+ "version": "2.4.1",
4
4
  "description": "大虾皮金融数据API命令行工具",
5
5
  "bin": {
6
6
  "daxiapi": "./bin/index.js",
7
7
  "dxp": "./bin/index.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "node bin/index.js --help"
10
+ "test": "node --test test/*.test.js",
11
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-branches=80 --test-coverage-functions=80 --test-coverage-lines=80 test/*.test.js"
11
12
  },
12
13
  "keywords": [
13
14
  "stock",
@@ -20,8 +21,6 @@
20
21
  "license": "MIT",
21
22
  "dependencies": {
22
23
  "iconv-lite": "^0.5.0",
23
- "request": "^2.88.0",
24
- "request-promise-native": "^1.0.7",
25
24
  "@ksky521/html-picker": "^1.0.0",
26
25
  "@toon-format/toon": "^2.1.0",
27
26
  "axios": "^1.6.0",
@@ -38,4 +37,4 @@
38
37
  "lib",
39
38
  "README.md"
40
39
  ]
41
- }
40
+ }