daxiapi-cli 2.1.0 → 2.3.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/lib/api.js CHANGED
@@ -1,24 +1,12 @@
1
1
  const axios = require('axios');
2
2
  const config = require('./config');
3
+ const request = require('./request');
4
+ const {formatThsVolumeTime, isTradingNow} = require('./utils');
5
+ const {calculateScores} = require('./dividendUtils');
6
+ const getFinanceReportDetail = require('./caibao');
3
7
 
4
8
  const BASE_URL = config.get('baseUrl') || 'https://daxiapi.com';
5
9
 
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
-
22
10
  function createClient(token) {
23
11
  return axios.create({
24
12
  baseURL: `${BASE_URL}/coze`,
@@ -138,14 +126,18 @@ async function getDividendScore(token, code) {
138
126
  throw new Error('Invalid code: code must be a non-empty string');
139
127
  }
140
128
 
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');
129
+ let rawData = await axios.get(`${BASE_URL}/sk/${code}.json`);
130
+ if (!rawData.data) {
131
+ throw new Error('Failed to get kline data');
145
132
  }
146
-
147
- const {klines} = klineData;
148
- const scores = calculateScores(klines);
133
+ const klineData = rawData.data;
134
+ const klines = klineData.k || '';
135
+ const scores = calculateScores(
136
+ klines.split(';').map(a => {
137
+ const [date, open, close, high, low, volume] = a.split(',');
138
+ return {date, close: Number(close), open: Number(open), high, low, vol: Number(volume)};
139
+ })
140
+ );
149
141
  const recentScores = scores.slice(-60);
150
142
 
151
143
  return {
@@ -154,202 +146,197 @@ async function getDividendScore(token, code) {
154
146
  scores: recentScores.map(item => ({
155
147
  date: item.date,
156
148
  score: item.totalScore,
157
- cs: item.cs,
158
- rsi: item.rsi
149
+ cs: item.cs.toFixed(2),
150
+ rsi: item.rsi.toFixed(2)
159
151
  }))
160
152
  };
161
153
  }
162
154
 
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);
155
+ async function getStockRank(type = 'hour', listType = 'normal') {
156
+ try {
157
+ const params = {stock_type: 'a', list_type: listType};
168
158
 
169
- for (let i = 0; i < closes.length; i++) {
170
- if (i === 0) {
171
- ema = closes[i];
159
+ // 只有 normal skyrocket 才有 type 参数,默认为 hour
160
+ // 其他榜单类型固定为 day
161
+ if (listType === 'normal' || listType === 'skyrocket') {
162
+ params.type = type;
172
163
  } else {
173
- ema = (closes[i] - ema) * multiplier + ema;
164
+ params.type = 'day';
174
165
  }
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
- }
166
+ const response = await request.get('/fuyao/hot_list_data/out/hot_list/v1/stock', params);
167
+ const data = extractData(response, 'stock_list');
168
+
169
+ return data.map(a => {
170
+ const result = {
171
+ code: a.code,
172
+ name: a.name,
173
+ 涨跌幅: a.rise_and_fall,
174
+ 讨论热度: a.rate,
175
+ 热榜变化: a.hot_rank_chg,
176
+ 排名: a.display_order != null ? a.display_order : a.order != null ? a.order : 0
177
+ };
178
+
179
+ if (listType === 'normal' || listType === 'skyrocket') {
180
+ result.上涨原因 = a.analyse_title;
181
+ result.上涨分析 = a.analyse
182
+ ?.split('\n')
183
+ .map(line => {
184
+ if (line.includes('免责声明')) {
185
+ return '';
186
+ }
187
+ return line.trim();
188
+ })
189
+ .filter(line => line !== '')
190
+ .join('\n');
226
191
  }
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
192
 
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
- }
193
+ return result;
194
+ });
195
+ } catch (err) {
196
+ console.error('[getStockRank] 获取热股榜数据失败:', err);
197
+ throw err;
242
198
  }
243
-
244
- return rsiValues;
245
199
  }
246
200
 
247
- function percentile(p, arr) {
248
- if (!arr.length) {
249
- return 0;
201
+ async function getPlateRank(type = 'concept') {
202
+ try {
203
+ const response = await request.get('/fuyao/hot_list_data/out/hot_list/v1/plate', {type});
204
+ const data = extractData(response, 'plate_list');
205
+
206
+ 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
+ return {
221
+ code: d.code,
222
+ name: d.name,
223
+ 涨跌幅: d.rise_and_fall,
224
+ 对应etf涨跌幅: d.etf_rise_and_fall,
225
+ 热榜涨跌幅: d.hot_rank_chg,
226
+ 热榜标签: d.hot_tag,
227
+ 对应etf代码: d.etf_product_id,
228
+ 讨论热度: d.rate,
229
+ 对应etf名称: d.etf_name,
230
+ tag: d.tag,
231
+ 排名: d.display_order != null ? d.display_order : d.order != null ? d.order : 0
232
+ };
233
+ });
234
+ } catch (err) {
235
+ console.error('[getPlateRank] 获取板块热榜数据失败:', err);
236
+ throw err;
250
237
  }
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
238
  }
255
239
 
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;
240
+ function extractData(response, listKey) {
241
+ if (response && response.data && response.data[listKey] && Array.isArray(response.data[listKey])) {
242
+ return response.data[listKey];
260
243
  }
261
-
262
- const low = percentile(lowPercentile, validValues);
263
- const high = percentile(highPercentile, validValues);
264
-
265
- if (low === high) {
266
- return 50;
244
+ if (Array.isArray(response)) {
245
+ return response;
267
246
  }
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));
247
+ console.warn('[extractData] 未知的数据格式:', response);
248
+ return [];
272
249
  }
273
250
 
274
- function calculateScores(data) {
275
- const dataCopy = data.map(item => ({...item}));
251
+ async function getTurnoverData(type = 'day') {
252
+ try {
253
+ const minuteData = await getTurnoverDataByMinute();
276
254
 
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);
255
+ const data = await request.get('/fuyao/market_analysis_api/chart/v1/get_chart_data', {
256
+ chart_key: 'turnover_day'
257
+ });
281
258
 
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
- }
259
+ if (data && data.status_code === 0 && data.data && data.data.charts) {
260
+ const charts = data.data.charts;
261
+ const pointList = charts.point_list;
287
262
 
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
- }
263
+ if (!pointList || pointList.length < 2) {
264
+ throw new Error('数据不足');
265
+ }
301
266
 
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;
267
+ const latest = pointList[pointList.length - 1];
268
+ const previous = pointList[pointList.length - 2];
269
+
270
+ const currentTurnover = latest[1];
271
+ const prevTurnover = previous[1];
272
+ 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); // 转为亿
276
+ const formattedData = {
277
+ 当前成交额: currentWanYi + '万亿',
278
+ 变化量: (diff > 0 ? '增加' : '减少') + diffYi + '亿',
279
+ 较上日: diff > 0 ? '增加' : '减少'
280
+ };
281
+ 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
+ const rs = {};
287
+ minuteData.header.forEach((item, index) => {
288
+ rs[item.name] = item.val;
289
+ });
290
+
291
+ if (isTrading) {
292
+ return {
293
+ 是否正在盘中交易: isTrading ? '是' : '否',
294
+ ...rs,
295
+ minuteData
296
+ };
297
+ }
298
+ return {
299
+ ...formattedData,
300
+ 是否正在盘中交易: isTrading ? '是' : '否',
301
+ ...rs,
302
+ minuteData
303
+ };
330
304
  }
305
+
306
+ throw new Error('数据格式错误');
307
+ } catch (err) {
308
+ console.error('[getTurnoverData] 获取成交额数据失败:', err);
309
+ throw err;
331
310
  }
311
+ }
332
312
 
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;
313
+ async function getTurnoverDataByMinute() {
314
+ try {
315
+ const data = await request.get('/fuyao/market_analysis_api/chart/v1/get_chart_data', {
316
+ chart_key: 'turnover_minute'
317
+ });
318
+
319
+ if (data && data.status_code === 0 && data.data && data.data.charts) {
320
+ const charts = data.data.charts;
321
+ const e = formatThsVolumeTime(data.data);
322
+ const isTrading = isTradingNow(e.dataTimestamp);
323
+ return {
324
+ isTrading,
325
+ name: charts.name,
326
+ time: charts.mtime,
327
+ header: charts.header,
328
+ point_key_list: charts.point_key_list,
329
+ point_list: charts.point_list,
330
+ lines: charts.lines,
331
+ x_label_list: charts.x_label_list
332
+ };
338
333
  }
339
334
 
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
- }
335
+ throw new Error('数据格式错误');
336
+ } catch (err) {
337
+ console.error('[getTurnoverDataByMinute] 获取成交额数据失败:', err);
338
+ throw err;
350
339
  }
351
-
352
- return dataCopy;
353
340
  }
354
341
 
355
342
  module.exports = {
@@ -369,5 +356,10 @@ module.exports = {
369
356
  getSecId,
370
357
  queryStockData,
371
358
  getPatternStocks,
372
- getDividendScore
359
+ getDividendScore,
360
+ getStockRank,
361
+ getPlateRank,
362
+ getTurnoverData,
363
+ getTurnoverDataByMinute,
364
+ getFinanceReportDetail
373
365
  };
package/lib/caibao.js ADDED
@@ -0,0 +1,69 @@
1
+ // http://basic.10jqka.com.cn/300014/
2
+ // const vm = require('vm');
3
+
4
+ // ?ut=&invt=2&fltt=2&fields=&secid=1.600862&cb=&_=1601168994587
5
+ const picker = require('@ksky521/html-picker');
6
+ const iconv = require('./iconv');
7
+
8
+ const {getHeader} = require('./thsUtils');
9
+
10
+ const request = require('request-promise-native');
11
+
12
+ const API_URL = 'http://basic.10jqka.com.cn/';
13
+
14
+ function getDetail(stockId) {
15
+ const prefix = `${API_URL}${stockId}`;
16
+ const url = `${prefix}/finance.html`;
17
+ // console.log(url);
18
+ return request({
19
+ uri: url,
20
+ encoding: null,
21
+ 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
+ }
31
+ }
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
+ }
50
+ }
51
+ if (Object.keys(report).length > 0) {
52
+ reports.push(report);
53
+ }
54
+ }
55
+ // console.log(titles.length, content.length);
56
+ return reports;
57
+ });
58
+ }
59
+
60
+ module.exports = getDetail;
61
+ // if (require.main === module) {
62
+ // // getDetail('300014').then(data => console.log(data));
63
+ // // getDetail('600036').then(data => {
64
+ // // const title = data.title;
65
+ // // const content = data.simple;
66
+ // // console.log(data.title[13], content[13]);
67
+ // // });
68
+ // // getDetail('601633').then(data => console.log(data));
69
+ // }