daxiapi-cli 2.0.1 → 2.1.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 CHANGED
@@ -73,11 +73,17 @@ daxiapi sector bk
73
73
  # 板块内个股排名
74
74
  daxiapi sector stocks --code BK0477
75
75
 
76
- # 热门股票
76
+ # 热门股票(各板块领涨股)
77
77
  daxiapi sector top
78
78
 
79
- # 热门概念
79
+ # 热门概念板块
80
80
  daxiapi sector gn
81
+
82
+ # 使用同花顺数据源(默认)
83
+ daxiapi sector gn --type ths
84
+
85
+ # 使用东方财富数据源
86
+ daxiapi sector gn --type dfcf
81
87
  ```
82
88
 
83
89
  ### 股票数据
@@ -95,6 +101,50 @@ daxiapi stock 000001 600031 300750
95
101
 
96
102
  # 概念股查询
97
103
  daxiapi stock gn GN1234
104
+
105
+ # 技术形态筛选股票
106
+ daxiapi stock pattern vcp
107
+ daxiapi stock pattern rps
108
+ daxiapi stock pattern newHigh
109
+ ```
110
+
111
+ #### 支持的技术形态(pattern)
112
+
113
+ | 形态代码 | 说明 |
114
+ |---------|------|
115
+ | **价值指标类** | |
116
+ | gxl | 股息率大于3%的股票 |
117
+ | **强度指标类** | |
118
+ | rps | RPS大于70的股票(欧奈尔RPS指标) |
119
+ | sctr | SCTR大于70的股票(Stockcharts Rank相对强度排序) |
120
+ | rpsTop3 | RPS行业前三 |
121
+ | csTop3 | CS行业前三 |
122
+ | sctrTop3 | SCTR行业前三 |
123
+ | **趋势形态类** | |
124
+ | trendUp | K线趋势向上 |
125
+ | high_60d | 大量创60日新高 |
126
+ | newHigh | 新高附近 |
127
+ | crossMa50 | 上穿MA50 |
128
+ | crossoverBox | 价格行为交易法信号K,上穿箱体 |
129
+ | cs_crossover_20 | CS穿过MA20 |
130
+ | **成交量形态类** | |
131
+ | fangliang | 放量上涨(前一天是VCP/3C形态,当天放量突破收盘在高点) |
132
+ | fangliangtupo | 放量突破箱体 |
133
+ | **涨跌幅排名类** | |
134
+ | zdf1dTop3 | 1日涨幅行业前三 |
135
+ | zdf5dTop3 | 5日涨幅行业前三 |
136
+ | zdf10dTop3 | 10日涨幅行业前三 |
137
+ | zdf20dTop3 | 20日涨幅行业前三 |
138
+ | shizhiTop3 | 行业市值前三 |
139
+ | **经典技术形态类** | |
140
+ | vcp | 股魔VCP形态(波动收缩形态) |
141
+ | joc | 跨越小溪Joc |
142
+ | sos | 强势上涨SOS |
143
+ | sos_h1 | SOS之后出现高1入场点 |
144
+ | spring | Spring弹簧形态(上涨波段回调后出现spring向上) |
145
+ | w | SOS之后出现W底吸收 |
146
+ | lps | LPS最后供应点(SOS之后的LPS) |
147
+ | ibs | K线实体较大(当日放量上涨收盘超昨日高点,实体长度超当日幅度69%) |
98
148
  ```
99
149
 
100
150
  ### K线数据
package/bin/index.js CHANGED
@@ -17,5 +17,6 @@ require('../commands/kline')(program);
17
17
  require('../commands/zdt')(program);
18
18
  require('../commands/secid')(program);
19
19
  require('../commands/search')(program);
20
+ require('../commands/dividend')(program);
20
21
 
21
22
  program.parse(process.argv);
@@ -0,0 +1,35 @@
1
+ const config = require('../lib/config');
2
+ const api = require('../lib/api');
3
+ const {handleError} = require('../lib/error');
4
+ const {output} = require('../lib/output');
5
+
6
+ module.exports = function (program) {
7
+ const dividendCmd = program.command('dividend');
8
+
9
+ dividendCmd
10
+ .command('score')
11
+ .description(
12
+ '获取红利类指数的打分数据,包括红利低波、红利低波100、中证红利、中证现金流等指数的打分情况。可用于判断红利类指数的超买超卖状态和投资机会。'
13
+ )
14
+ .option(
15
+ '-c, --code <code>',
16
+ '指数代码,例如:2.H30269(红利低波)、2.930955(红利低波100)、1.000922(中证红利)、2.932365(中证现金流)',
17
+ '2.H30269'
18
+ )
19
+ .action(async options => {
20
+ try {
21
+ const token = config.getToken();
22
+ if (!token) {
23
+ const error = new Error('未配置 API Token');
24
+ error.response = {status: 401};
25
+ throw error;
26
+ }
27
+
28
+ const data = await api.getDividendScore(token, options.code);
29
+ output(data);
30
+ } catch (error) {
31
+ handleError(error);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ };
package/commands/kline.js CHANGED
@@ -3,11 +3,13 @@ const api = require('../lib/api');
3
3
  const {handleError, createParameterError} = require('../lib/error');
4
4
  const {output} = require('../lib/output');
5
5
 
6
- module.exports = function(program) {
6
+ module.exports = function (program) {
7
7
  program
8
8
  .command('kline <code>')
9
- .description('获取A股股票、指数、板块的K线数据,支持股票代码、指数代码、板块代码。默认返回上证指数(000001)的K线,可指定K线条数(1-500,默认60)。返回开盘价、收盘价、最高价、最低价、成交量等K线数据,可用于技术分析和趋势判断。')
10
- .action(async (code) => {
9
+ .description(
10
+ '获取A股股票、指数、板块的K线数据,支持股票代码、指数代码、板块代码。默认返回上证指数(000001)的K线,可指定K线条数(1-500,默认60)。返回开盘价、收盘价、最高价、最低价、成交量等K线数据,可用于技术分析和趋势判断。'
11
+ )
12
+ .action(async code => {
11
13
  try {
12
14
  const token = config.getToken();
13
15
  if (!token) {
@@ -17,11 +19,7 @@ module.exports = function(program) {
17
19
  }
18
20
 
19
21
  if (!code) {
20
- throw createParameterError(
21
- '参数无效',
22
- ['参数 \'code\' 不能为空'],
23
- ['daxiapi kline 000001']
24
- );
22
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], ['daxiapi kline 000001']);
25
23
  }
26
24
 
27
25
  const data = await api.getKline(token, code);
@@ -93,4 +93,6 @@ module.exports = function (program) {
93
93
  process.exit(1);
94
94
  }
95
95
  });
96
+
97
+
96
98
  };
package/commands/secid.js CHANGED
@@ -1,30 +1,20 @@
1
- const config = require('../lib/config');
2
- const api = require('../lib/api');
3
1
  const {handleError, createParameterError} = require('../lib/error');
4
2
  const {output} = require('../lib/output');
3
+ const {getSecid} = require('../lib/utils');
5
4
 
6
- module.exports = function(program) {
5
+ module.exports = function (program) {
7
6
  program
8
7
  .command('secid <code>')
9
- .description('将各种股票代码格式转换为标准secid格式,支持以下格式:6位数字股票代码(000001)、sh/sz前缀(sh000001)、BK开头板块代码(BK0428)、纯数字板块代码(428)。返回标准secid格式,如1.600000(沪市)、0.000001(深市)、90.BK0428(板块)。可用于统一代码格式和K线数据查询。')
10
- .action(async (code) => {
8
+ .description(
9
+ '将各种股票代码格式转换为标准secid格式,支持以下格式:6位数字股票代码(000001)、sh/sz前缀(sh000001)、BK开头板块代码(BK0428)、纯数字板块代码(428)。返回标准secid格式,如1.600000(沪市)、0.000001(深市)、90.BK0428(板块)。可用于统一代码格式和K线数据查询。'
10
+ )
11
+ .action(async code => {
11
12
  try {
12
- const token = config.getToken();
13
- if (!token) {
14
- const error = new Error('未配置 API Token');
15
- error.response = {status: 401};
16
- throw error;
17
- }
18
-
19
13
  if (!code) {
20
- throw createParameterError(
21
- '参数无效',
22
- ["参数 'code' 不能为空"],
23
- ['daxiapi secid 000001']
24
- );
14
+ throw createParameterError('参数无效', ["参数 'code' 不能为空"], ['daxiapi secid 000001']);
25
15
  }
26
16
 
27
- const data = await api.getSecId(token, code);
17
+ const data = getSecid(code);
28
18
  output(data);
29
19
  } catch (error) {
30
20
  handleError(error);
package/commands/stock.js CHANGED
@@ -3,14 +3,47 @@ const api = require('../lib/api');
3
3
  const {handleError, createParameterError} = require('../lib/error');
4
4
  const {output} = require('../lib/output');
5
5
 
6
- module.exports = function(program) {
6
+ const SUPPORTED_PATTERNS = [
7
+ 'gxl',
8
+ 'rps',
9
+ 'sctr',
10
+ 'trendUp',
11
+ 'high_60d',
12
+ 'crossMa50',
13
+ 'rpsTop3',
14
+ 'sos_h1',
15
+ 'csTop3',
16
+ 'sctrTop3',
17
+ 'newHigh',
18
+ 'fangliang',
19
+ 'shizhiTop3',
20
+ 'zdf5dTop3',
21
+ 'zdf1dTop3',
22
+ 'zdf10dTop3',
23
+ 'zdf20dTop3',
24
+ 'ibs',
25
+ 'vcp',
26
+ 'joc',
27
+ 'sos',
28
+ 'spring',
29
+ 'w',
30
+ 'fangliangtupo',
31
+ 'crossoverBox',
32
+ 'lps',
33
+ 'cs_crossover_20'
34
+ ];
35
+
36
+ module.exports = function (program) {
7
37
  const stockCmd = program.command('stock');
8
38
 
9
39
  stockCmd
10
40
  .command('info')
11
- .description('根据股票代码获取A股详细信息,支持多个代码用逗号分隔(最多20只),支持6位数字代码和116.xxx格式。返回股票名称、代码、涨跌幅、CS强度、SCTR排名、所属板块、概念、市值、成交额等详细数据。可用于个股分析和多只股票对比。')
41
+ .description(
42
+ '根据股票代码获取A股详细信息,支持多个代码用逗号分隔(最多20只),支持6位数字代码和116.xxx格式。' +
43
+ '返回股票名称、代码、涨跌幅、CS强度、SCTR排名、所属板块、概念、市值、成交额等详细数据。可用于个股分析和多只股票对比。'
44
+ )
12
45
  .argument('<codes...>', '股票代码(支持多个)')
13
- .action(async (codes) => {
46
+ .action(async codes => {
14
47
  try {
15
48
  const token = config.getToken();
16
49
  if (!token) {
@@ -23,10 +56,7 @@ module.exports = function(program) {
23
56
  throw createParameterError(
24
57
  '参数无效',
25
58
  ["参数 'codes' 不能为空"],
26
- [
27
- 'daxiapi stock info 000001',
28
- 'daxiapi stock info 000001 600031 300750'
29
- ]
59
+ ['daxiapi stock info 000001', 'daxiapi stock info 000001 600031 300750']
30
60
  );
31
61
  }
32
62
 
@@ -40,7 +70,11 @@ module.exports = function(program) {
40
70
 
41
71
  stockCmd
42
72
  .command('gn <gnId>')
43
- .description('根据概念板块ID获取该概念下的所有股票数据,支持同花顺(881155)和东方财富(BK0428)两种格式的板块ID。自动根据ID格式选择数据源,返回股票名称、代码、涨跌幅、CS强度、SCTR排名等详细信息,最多返回300只股票。可用于概念板块成分股分析和板块内股票筛选。')
73
+ .description(
74
+ '根据概念板块ID获取该概念下的所有股票数据,支持同花顺(881155)和东方财富(BK0428)两种格式的板块ID。' +
75
+ '自动根据ID格式选择数据源,返回股票名称、代码、涨跌幅、CS强度、SCTR排名等详细信息,最多返回300只股票。' +
76
+ '可用于概念板块成分股分析和板块内股票筛选。'
77
+ )
44
78
  .option('--type <type>', '数据源类型 (dfcf|ths)', 'ths')
45
79
  .action(async (gnId, options) => {
46
80
  try {
@@ -52,21 +86,14 @@ module.exports = function(program) {
52
86
  }
53
87
 
54
88
  if (!gnId) {
55
- throw createParameterError(
56
- '参数无效',
57
- ["参数 'gnId' 不能为空"],
58
- ['daxiapi stock gn GN1234']
59
- );
89
+ throw createParameterError('参数无效', ["参数 'gnId' 不能为空"], ['daxiapi stock gn GN1234']);
60
90
  }
61
91
 
62
92
  if (!['dfcf', 'ths'].includes(options.type)) {
63
93
  throw createParameterError(
64
94
  '参数无效',
65
95
  ["参数 'type' 必须是 dfcf 或 ths"],
66
- [
67
- 'daxiapi stock gn GN1234 --type ths',
68
- 'daxiapi stock gn GN1234 --type dfcf'
69
- ]
96
+ ['daxiapi stock gn GN1234 --type ths', 'daxiapi stock gn GN1234 --type dfcf']
70
97
  );
71
98
  }
72
99
 
@@ -77,4 +104,40 @@ module.exports = function(program) {
77
104
  process.exit(1);
78
105
  }
79
106
  });
107
+
108
+ stockCmd
109
+ .command('pattern <pattern>')
110
+ .description(
111
+ '根据技术形态筛选股票,支持27种形态类型。包括强度指标类(gxl/rps/sctr等)、趋势形态类(trendUp/high_60d/newHigh等)、' +
112
+ '成交量形态类(fangliang/fangliangtupo)、涨跌幅排名类(zdf1dTop3等)、经典技术形态类(vcp/joc/sos/spring等)。' +
113
+ '返回符合指定形态的股票列表,包括代码、名称、涨幅、RPS、SCTR、CS强度等指标。可用于技术形态选股和量化策略筛选。'
114
+ )
115
+ .action(async pattern => {
116
+ try {
117
+ const token = config.getToken();
118
+ if (!token) {
119
+ const error = new Error('未配置 API Token');
120
+ error.response = {status: 401};
121
+ throw error;
122
+ }
123
+
124
+ if (!pattern) {
125
+ throw createParameterError('参数无效', ["参数 'pattern' 不能为空"], ['daxiapi stock pattern vcp']);
126
+ }
127
+
128
+ if (!SUPPORTED_PATTERNS.includes(pattern)) {
129
+ throw createParameterError(
130
+ '参数无效',
131
+ [`不支持的形态类型: ${pattern}`, `支持的形态: ${SUPPORTED_PATTERNS.join(', ')}`],
132
+ ['daxiapi stock pattern vcp', 'daxiapi stock pattern rps', 'daxiapi stock pattern newHigh']
133
+ );
134
+ }
135
+
136
+ const data = await api.getPatternStocks(token, pattern);
137
+ output(data);
138
+ } catch (error) {
139
+ handleError(error);
140
+ process.exit(1);
141
+ }
142
+ });
80
143
  };
package/lib/api.js CHANGED
@@ -3,11 +3,27 @@ const config = require('./config');
3
3
 
4
4
  const BASE_URL = config.get('baseUrl') || 'https://daxiapi.com';
5
5
 
6
+ const DIVIDEND_SCORE_CONSTANTS = {
7
+ ROLLING_WINDOW: 440,
8
+ PERCENTILE_LOW: 5,
9
+ PERCENTILE_HIGH: 95,
10
+ SCORE_MA_PERIOD: 5,
11
+ EMA_PERIOD: 20,
12
+ MA_PERIOD: 80,
13
+ RSI_PERIOD: 20,
14
+ MIN_VALID_VALUES: 10,
15
+ MIN_SCORE: 0,
16
+ MAX_SCORE: 100,
17
+ CS_WEIGHT: 0.35,
18
+ MA80_WEIGHT: 0.35,
19
+ RSI_WEIGHT: 0.3
20
+ };
21
+
6
22
  function createClient(token) {
7
23
  return axios.create({
8
24
  baseURL: `${BASE_URL}/coze`,
9
25
  headers: {
10
- 'Authorization': `Bearer ${token}`,
26
+ Authorization: `Bearer ${token}`,
11
27
  'Content-Type': 'application/json'
12
28
  },
13
29
  timeout: 30000
@@ -109,6 +125,233 @@ async function queryStockData(token, q, type = 'stock') {
109
125
  return post(client, '/query_stock_data', {q, type});
110
126
  }
111
127
 
128
+ async function getPatternStocks(token, pattern) {
129
+ const client = createClient(token);
130
+ return post(client, '/get_pattern_stocks', {pattern});
131
+ }
132
+
133
+ async function getDividendScore(token, code) {
134
+ if (!token || typeof token !== 'string') {
135
+ throw new Error('Invalid token: token must be a non-empty string');
136
+ }
137
+ if (!code || typeof code !== 'string') {
138
+ throw new Error('Invalid code: code must be a non-empty string');
139
+ }
140
+
141
+ const klineData = await getKline(token, code);
142
+
143
+ if (!klineData || !Array.isArray(klineData.klines)) {
144
+ throw new Error('Invalid kline data structure: klines must be an array');
145
+ }
146
+
147
+ const {klines} = klineData;
148
+ const scores = calculateScores(klines);
149
+ const recentScores = scores.slice(-60);
150
+
151
+ return {
152
+ code: code,
153
+ name: klineData.name || '未知指数',
154
+ scores: recentScores.map(item => ({
155
+ date: item.date,
156
+ score: item.totalScore,
157
+ cs: item.cs,
158
+ rsi: item.rsi
159
+ }))
160
+ };
161
+ }
162
+
163
+ function calculateEMA(period, data) {
164
+ const closes = data.map(d => d.close);
165
+ const cs = [];
166
+ let ema = closes[0];
167
+ const multiplier = 2 / (period + 1);
168
+
169
+ for (let i = 0; i < closes.length; i++) {
170
+ if (i === 0) {
171
+ ema = closes[i];
172
+ } else {
173
+ ema = (closes[i] - ema) * multiplier + ema;
174
+ }
175
+ cs.push(((closes[i] - ema) / ema) * 100);
176
+ }
177
+
178
+ return {cs};
179
+ }
180
+
181
+ function calculateMA(period, data) {
182
+ const closes = data.map(d => d.close);
183
+ const ma = [];
184
+ const maBias = [];
185
+
186
+ for (let i = 0; i < closes.length; i++) {
187
+ if (i < period - 1) {
188
+ ma.push('-');
189
+ maBias.push('-');
190
+ } else {
191
+ let sum = 0;
192
+ for (let j = 0; j < period; j++) {
193
+ sum += closes[i - j];
194
+ }
195
+ const avg = sum / period;
196
+ ma.push(avg);
197
+ maBias.push(((closes[i] - avg) / avg) * 100);
198
+ }
199
+ }
200
+
201
+ return [ma, maBias];
202
+ }
203
+
204
+ function calculateRSI(closes, period = 20) {
205
+ const rsiValues = [];
206
+ let gains = 0;
207
+ let losses = 0;
208
+
209
+ for (let i = 0; i < closes.length; i++) {
210
+ if (i < period) {
211
+ rsiValues.push(null);
212
+ continue;
213
+ }
214
+
215
+ const change = closes[i] - closes[i - 1];
216
+ if (i === period) {
217
+ let sumGain = 0;
218
+ let sumLoss = 0;
219
+ for (let j = 1; j <= period; j++) {
220
+ const c = closes[j] - closes[j - 1];
221
+ if (c > 0) {
222
+ sumGain += c;
223
+ } else {
224
+ sumLoss += Math.abs(c);
225
+ }
226
+ }
227
+ gains = sumGain / period;
228
+ losses = sumLoss / period;
229
+ } else {
230
+ const currentGain = change > 0 ? change : 0;
231
+ const currentLoss = change < 0 ? Math.abs(change) : 0;
232
+ gains = (gains * (period - 1) + currentGain) / period;
233
+ losses = (losses * (period - 1) + currentLoss) / period;
234
+ }
235
+
236
+ if (gains + losses === 0) {
237
+ rsiValues.push(50);
238
+ } else {
239
+ const rs = gains / losses;
240
+ rsiValues.push(parseFloat((100 - 100 / (1 + rs)).toFixed(2)));
241
+ }
242
+ }
243
+
244
+ return rsiValues;
245
+ }
246
+
247
+ function percentile(p, arr) {
248
+ if (!arr.length) {
249
+ return 0;
250
+ }
251
+ const sorted = [...arr].sort((a, b) => a - b);
252
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
253
+ return sorted[Math.max(0, index)];
254
+ }
255
+
256
+ function calculateRollingScore(value, historyValues, lowPercentile, highPercentile) {
257
+ const validValues = historyValues.filter(v => v !== null && !isNaN(v));
258
+ if (validValues.length < DIVIDEND_SCORE_CONSTANTS.MIN_VALID_VALUES || value === null) {
259
+ return null;
260
+ }
261
+
262
+ const low = percentile(lowPercentile, validValues);
263
+ const high = percentile(highPercentile, validValues);
264
+
265
+ if (low === high) {
266
+ return 50;
267
+ }
268
+
269
+ let score = ((value - low) / (high - low)) * 100;
270
+ score = Math.max(DIVIDEND_SCORE_CONSTANTS.MIN_SCORE, Math.min(DIVIDEND_SCORE_CONSTANTS.MAX_SCORE, score));
271
+ return parseFloat(score.toFixed(2));
272
+ }
273
+
274
+ function calculateScores(data) {
275
+ const dataCopy = data.map(item => ({...item}));
276
+
277
+ const closes = dataCopy.map(d => d.close);
278
+ const {cs} = calculateEMA(DIVIDEND_SCORE_CONSTANTS.EMA_PERIOD, dataCopy);
279
+ const [_, ma80Bias] = calculateMA(DIVIDEND_SCORE_CONSTANTS.MA_PERIOD, dataCopy);
280
+ const rsi = calculateRSI(closes, DIVIDEND_SCORE_CONSTANTS.RSI_PERIOD);
281
+
282
+ for (let i = 0; i < dataCopy.length; i++) {
283
+ dataCopy[i].cs = cs[i] === '-' ? null : parseFloat(cs[i]);
284
+ dataCopy[i].ma80Bias = ma80Bias[i] === '-' ? null : parseFloat(ma80Bias[i]);
285
+ dataCopy[i].rsi = rsi[i];
286
+ }
287
+
288
+ const csValues = dataCopy.map(d => d.cs);
289
+ const ma80BiasValues = dataCopy.map(d => d.ma80Bias);
290
+
291
+ for (let i = 0; i < dataCopy.length; i++) {
292
+ const current = dataCopy[i];
293
+ if (current.cs === null || current.ma80Bias === null || current.rsi === null) {
294
+ current.csScore = null;
295
+ current.ma80Score = null;
296
+ current.rsiScore = null;
297
+ current.totalScore = null;
298
+ current.scoreMA = null;
299
+ continue;
300
+ }
301
+
302
+ const startIdx = Math.max(0, i - DIVIDEND_SCORE_CONSTANTS.ROLLING_WINDOW + 1);
303
+ const csHistory = csValues.slice(startIdx, i + 1);
304
+ const ma80History = ma80BiasValues.slice(startIdx, i + 1);
305
+
306
+ current.csScore = calculateRollingScore(
307
+ current.cs,
308
+ csHistory,
309
+ DIVIDEND_SCORE_CONSTANTS.PERCENTILE_LOW,
310
+ DIVIDEND_SCORE_CONSTANTS.PERCENTILE_HIGH
311
+ );
312
+ current.ma80Score = calculateRollingScore(
313
+ current.ma80Bias,
314
+ ma80History,
315
+ DIVIDEND_SCORE_CONSTANTS.PERCENTILE_LOW,
316
+ DIVIDEND_SCORE_CONSTANTS.PERCENTILE_HIGH
317
+ );
318
+ current.rsiScore = current.rsi;
319
+
320
+ if (current.csScore !== null && current.ma80Score !== null && current.rsiScore !== null) {
321
+ current.totalScore = parseFloat(
322
+ (
323
+ current.csScore * DIVIDEND_SCORE_CONSTANTS.CS_WEIGHT +
324
+ current.ma80Score * DIVIDEND_SCORE_CONSTANTS.MA80_WEIGHT +
325
+ current.rsiScore * DIVIDEND_SCORE_CONSTANTS.RSI_WEIGHT
326
+ ).toFixed(2)
327
+ );
328
+ } else {
329
+ current.totalScore = null;
330
+ }
331
+ }
332
+
333
+ for (let i = 0; i < dataCopy.length; i++) {
334
+ const current = dataCopy[i];
335
+ if (current.totalScore === null) {
336
+ current.scoreMA = null;
337
+ continue;
338
+ }
339
+
340
+ const scoreHistory = dataCopy
341
+ .slice(Math.max(0, i - DIVIDEND_SCORE_CONSTANTS.SCORE_MA_PERIOD + 1), i + 1)
342
+ .filter(d => d.totalScore !== null)
343
+ .map(d => d.totalScore);
344
+
345
+ if (scoreHistory.length >= DIVIDEND_SCORE_CONSTANTS.SCORE_MA_PERIOD) {
346
+ current.scoreMA = parseFloat((scoreHistory.reduce((a, b) => a + b, 0) / scoreHistory.length).toFixed(2));
347
+ } else {
348
+ current.scoreMA = current.totalScore;
349
+ }
350
+ }
351
+
352
+ return dataCopy;
353
+ }
354
+
112
355
  module.exports = {
113
356
  getMarketData,
114
357
  getMarketTemp,
@@ -124,5 +367,7 @@ module.exports = {
124
367
  getKline,
125
368
  getZdtPool,
126
369
  getSecId,
127
- queryStockData
370
+ queryStockData,
371
+ getPatternStocks,
372
+ getDividendScore
128
373
  };
package/lib/output.js CHANGED
@@ -1,5 +1,19 @@
1
1
  function output(data) {
2
- console.log(JSON.stringify(data, null, 2));
2
+ // 检查是否是dividend score结果
3
+ if (data && data.scores && Array.isArray(data.scores)) {
4
+ console.log(`查询到${data.name || data.code}的最近60个交易日打分情况:`);
5
+ console.log('');
6
+ console.log('```toon');
7
+ console.log('[60]{"日期","分数","cs值","rsi值"}:');
8
+
9
+ data.scores.forEach(item => {
10
+ console.log(` ${item.date},${item.score || '-'},${item.cs || '-'},${item.rsi || '-'}`);
11
+ });
12
+
13
+ console.log('```');
14
+ } else {
15
+ console.log(JSON.stringify(data, null, 2));
16
+ }
3
17
  }
4
18
 
5
19
  module.exports = {
package/lib/utils.js ADDED
@@ -0,0 +1,31 @@
1
+ function getSecid(code) {
2
+ code = String(code);
3
+ if (code.indexOf('.') !== -1) {
4
+ const parts = code.split('.');
5
+ const stockCode = parts[0];
6
+ const suffix = parts[1].toUpperCase();
7
+ if (suffix === 'SH') {
8
+ return `1.${stockCode}`;
9
+ } else if (suffix === 'SZ') {
10
+ return `0.${stockCode}`;
11
+ }
12
+ return code;
13
+ }
14
+ if (code[0] === 'B' && code[1] === 'K') {
15
+ return '90.' + code;
16
+ }
17
+ if (code.length < 6) {
18
+ return '90.BK' + ('0000' + code).slice(-4);
19
+ }
20
+ if (code[0] === '6' || code[0] === '5') {
21
+ return `1.${code}`;
22
+ }
23
+ if (code[0].toLowerCase() === 's') {
24
+ return code.replace(/^sh/i, '1.').replace(/^sz/i, '0.');
25
+ }
26
+ return `0.${code}`;
27
+ }
28
+
29
+ module.exports = {
30
+ getSecid
31
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daxiapi-cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "大虾皮金融数据API命令行工具",
5
5
  "bin": {
6
6
  "daxiapi": "./bin/index.js",
@@ -26,5 +26,11 @@
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=14.0.0"
29
- }
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "commands",
33
+ "lib",
34
+ "README.md"
35
+ ]
30
36
  }