daxiapi-cli 2.2.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`,
@@ -139,15 +127,17 @@ async function getDividendScore(token, code) {
139
127
  }
140
128
 
141
129
  let rawData = await axios.get(`${BASE_URL}/sk/${code}.json`);
142
- if(!rawData.data){
130
+ if (!rawData.data) {
143
131
  throw new Error('Failed to get kline data');
144
132
  }
145
133
  const klineData = rawData.data;
146
134
  const klines = klineData.k || '';
147
- const scores = calculateScores(klines.split(';').map(a=>{
148
- const [date, open,close, high, low, volume] = a.split(',');
149
- return {date, close: Number(close), open: Number(open), high, low, vol: Number(volume)};
150
- }));
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
+ );
151
141
  const recentScores = scores.slice(-60);
152
142
 
153
143
  return {
@@ -162,195 +152,191 @@ async function getDividendScore(token, code) {
162
152
  };
163
153
  }
164
154
 
165
- function calculateEMA(period, data) {
166
- const closes = data.map(d => d.close);
167
- const cs = [];
168
- let ema = closes[0];
169
- const multiplier = 2 / (period + 1);
170
-
171
- for (let i = 0; i < closes.length; i++) {
172
- if (i === 0) {
173
- ema = closes[i];
174
- } else {
175
- ema = (closes[i] - ema) * multiplier + ema;
176
- }
177
- cs.push(((closes[i] - ema) / ema) * 100);
178
- }
179
-
180
- return {cs};
181
- }
182
-
183
- function calculateMA(period, data) {
184
- const closes = data.map(d => d.close);
185
- const ma = [];
186
- const maBias = [];
155
+ async function getStockRank(type = 'hour', listType = 'normal') {
156
+ try {
157
+ const params = {stock_type: 'a', list_type: listType};
187
158
 
188
- for (let i = 0; i < closes.length; i++) {
189
- if (i < period - 1) {
190
- ma.push('-');
191
- maBias.push('-');
159
+ // 只有 normal skyrocket 才有 type 参数,默认为 hour
160
+ // 其他榜单类型固定为 day
161
+ if (listType === 'normal' || listType === 'skyrocket') {
162
+ params.type = type;
192
163
  } else {
193
- let sum = 0;
194
- for (let j = 0; j < period; j++) {
195
- sum += closes[i - j];
196
- }
197
- const avg = sum / period;
198
- ma.push(avg);
199
- maBias.push(((closes[i] - avg) / avg) * 100);
164
+ params.type = 'day';
200
165
  }
201
- }
202
-
203
- return [ma, maBias];
204
- }
205
-
206
- function calculateRSI(closes, period = 20) {
207
- const rsiValues = [];
208
- let gains = 0;
209
- let losses = 0;
210
-
211
- for (let i = 0; i < closes.length; i++) {
212
- if (i < period) {
213
- rsiValues.push(null);
214
- continue;
215
- }
216
-
217
- const change = closes[i] - closes[i - 1];
218
- if (i === period) {
219
- let sumGain = 0;
220
- let sumLoss = 0;
221
- for (let j = 1; j <= period; j++) {
222
- const c = closes[j] - closes[j - 1];
223
- if (c > 0) {
224
- sumGain += c;
225
- } else {
226
- sumLoss += Math.abs(c);
227
- }
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');
228
191
  }
229
- gains = sumGain / period;
230
- losses = sumLoss / period;
231
- } else {
232
- const currentGain = change > 0 ? change : 0;
233
- const currentLoss = change < 0 ? Math.abs(change) : 0;
234
- gains = (gains * (period - 1) + currentGain) / period;
235
- losses = (losses * (period - 1) + currentLoss) / period;
236
- }
237
192
 
238
- if (gains + losses === 0) {
239
- rsiValues.push(50);
240
- } else {
241
- const rs = gains / losses;
242
- rsiValues.push(parseFloat((100 - 100 / (1 + rs)).toFixed(2)));
243
- }
193
+ return result;
194
+ });
195
+ } catch (err) {
196
+ console.error('[getStockRank] 获取热股榜数据失败:', err);
197
+ throw err;
244
198
  }
245
-
246
- return rsiValues;
247
199
  }
248
200
 
249
- function percentile(p, arr) {
250
- if (!arr.length) {
251
- 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;
252
237
  }
253
- const sorted = [...arr].sort((a, b) => a - b);
254
- const index = Math.ceil((p / 100) * sorted.length) - 1;
255
- return sorted[Math.max(0, index)];
256
238
  }
257
239
 
258
- function calculateRollingScore(value, historyValues, lowPercentile, highPercentile) {
259
- const validValues = historyValues.filter(v => v !== null && !isNaN(v));
260
- if (validValues.length < DIVIDEND_SCORE_CONSTANTS.MIN_VALID_VALUES || value === null) {
261
- 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];
243
+ }
244
+ if (Array.isArray(response)) {
245
+ return response;
262
246
  }
247
+ console.warn('[extractData] 未知的数据格式:', response);
248
+ return [];
249
+ }
263
250
 
264
- const low = percentile(lowPercentile, validValues);
265
- const high = percentile(highPercentile, validValues);
251
+ async function getTurnoverData(type = 'day') {
252
+ try {
253
+ const minuteData = await getTurnoverDataByMinute();
266
254
 
267
- if (low === high) {
268
- return 50;
269
- }
255
+ const data = await request.get('/fuyao/market_analysis_api/chart/v1/get_chart_data', {
256
+ chart_key: 'turnover_day'
257
+ });
270
258
 
271
- let score = ((value - low) / (high - low)) * 100;
272
- score = Math.max(DIVIDEND_SCORE_CONSTANTS.MIN_SCORE, Math.min(DIVIDEND_SCORE_CONSTANTS.MAX_SCORE, score));
273
- return parseFloat(score.toFixed(2));
274
- }
259
+ if (data && data.status_code === 0 && data.data && data.data.charts) {
260
+ const charts = data.data.charts;
261
+ const pointList = charts.point_list;
275
262
 
276
- function calculateScores(data) {
277
- const dataCopy = data.map(item => ({...item}));
278
-
279
- const closes = dataCopy.map(d => d.close);
280
- const {cs} = calculateEMA(DIVIDEND_SCORE_CONSTANTS.EMA_PERIOD, dataCopy);
281
- const [_, ma80Bias] = calculateMA(DIVIDEND_SCORE_CONSTANTS.MA_PERIOD, dataCopy);
282
- const rsi = calculateRSI(closes, DIVIDEND_SCORE_CONSTANTS.RSI_PERIOD);
283
- for (let i = 0; i < dataCopy.length; i++) {
284
- dataCopy[i].cs = cs[i] === '-' ? null : parseFloat(cs[i]);
285
- dataCopy[i].ma80Bias = ma80Bias[i] === '-' ? null : parseFloat(ma80Bias[i]);
286
- dataCopy[i].rsi = rsi[i];
287
- }
263
+ if (!pointList || pointList.length < 2) {
264
+ throw new Error('数据不足');
265
+ }
288
266
 
289
- const csValues = dataCopy.map(d => d.cs);
290
- const ma80BiasValues = dataCopy.map(d => d.ma80Bias);
291
-
292
- for (let i = 0; i < dataCopy.length; i++) {
293
- const current = dataCopy[i];
294
- if (current.cs === null || current.ma80Bias === null || current.rsi === null) {
295
- current.csScore = null;
296
- current.ma80Score = null;
297
- current.rsiScore = null;
298
- current.totalScore = null;
299
- current.scoreMA = null;
300
- continue;
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
+ };
301
304
  }
302
305
 
303
- const startIdx = Math.max(0, i - DIVIDEND_SCORE_CONSTANTS.ROLLING_WINDOW + 1);
304
- const csHistory = csValues.slice(startIdx, i + 1);
305
- const ma80History = ma80BiasValues.slice(startIdx, i + 1);
306
-
307
- current.csScore = calculateRollingScore(
308
- current.cs,
309
- csHistory,
310
- DIVIDEND_SCORE_CONSTANTS.PERCENTILE_LOW,
311
- DIVIDEND_SCORE_CONSTANTS.PERCENTILE_HIGH
312
- );
313
- current.ma80Score = calculateRollingScore(
314
- current.ma80Bias,
315
- ma80History,
316
- DIVIDEND_SCORE_CONSTANTS.PERCENTILE_LOW,
317
- DIVIDEND_SCORE_CONSTANTS.PERCENTILE_HIGH
318
- );
319
- current.rsiScore = current.rsi;
320
-
321
- if (current.csScore !== null && current.ma80Score !== null && current.rsiScore !== null) {
322
- current.totalScore = parseFloat(
323
- (
324
- current.csScore * DIVIDEND_SCORE_CONSTANTS.CS_WEIGHT +
325
- current.ma80Score * DIVIDEND_SCORE_CONSTANTS.MA80_WEIGHT +
326
- current.rsiScore * DIVIDEND_SCORE_CONSTANTS.RSI_WEIGHT
327
- ).toFixed(2)
328
- );
329
- } else {
330
- current.totalScore = null;
331
- }
306
+ throw new Error('数据格式错误');
307
+ } catch (err) {
308
+ console.error('[getTurnoverData] 获取成交额数据失败:', err);
309
+ throw err;
332
310
  }
311
+ }
333
312
 
334
- for (let i = 0; i < dataCopy.length; i++) {
335
- const current = dataCopy[i];
336
- if (current.totalScore === null) {
337
- current.scoreMA = null;
338
- 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
+ };
339
333
  }
340
334
 
341
- const scoreHistory = dataCopy
342
- .slice(Math.max(0, i - DIVIDEND_SCORE_CONSTANTS.SCORE_MA_PERIOD + 1), i + 1)
343
- .filter(d => d.totalScore !== null)
344
- .map(d => d.totalScore);
345
-
346
- if (scoreHistory.length >= DIVIDEND_SCORE_CONSTANTS.SCORE_MA_PERIOD) {
347
- current.scoreMA = parseFloat((scoreHistory.reduce((a, b) => a + b, 0) / scoreHistory.length).toFixed(2));
348
- } else {
349
- current.scoreMA = current.totalScore;
350
- }
335
+ throw new Error('数据格式错误');
336
+ } catch (err) {
337
+ console.error('[getTurnoverDataByMinute] 获取成交额数据失败:', err);
338
+ throw err;
351
339
  }
352
-
353
- return dataCopy;
354
340
  }
355
341
 
356
342
  module.exports = {
@@ -370,5 +356,10 @@ module.exports = {
370
356
  getSecId,
371
357
  queryStockData,
372
358
  getPatternStocks,
373
- getDividendScore
359
+ getDividendScore,
360
+ getStockRank,
361
+ getPlateRank,
362
+ getTurnoverData,
363
+ getTurnoverDataByMinute,
364
+ getFinanceReportDetail
374
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
+ // }