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/README.md +60 -4
- package/bin/index.js +3 -0
- package/commands/config.js +7 -17
- package/commands/dividend.js +3 -1
- package/commands/hotrank.js +63 -0
- package/commands/market.js +5 -3
- package/commands/report.js +32 -0
- package/commands/sector.js +5 -1
- package/commands/stock.js +4 -2
- package/commands/turnover.js +18 -0
- package/lib/api.js +183 -191
- package/lib/caibao.js +69 -0
- package/lib/dividendUtils.js +216 -0
- package/lib/iconv.js +4 -0
- package/lib/output.js +6 -4
- package/lib/request.js +44 -0
- package/lib/thsUtils.js +148 -0
- package/lib/utils.js +58 -2
- package/package.json +4 -1
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
148
|
-
const scores = calculateScores(
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
// 只有 normal 和 skyrocket 才有 type 参数,默认为 hour
|
|
160
|
+
// 其他榜单类型固定为 day
|
|
161
|
+
if (listType === 'normal' || listType === 'skyrocket') {
|
|
162
|
+
params.type = type;
|
|
172
163
|
} else {
|
|
173
|
-
|
|
164
|
+
params.type = 'day';
|
|
174
165
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
275
|
-
|
|
251
|
+
async function getTurnoverData(type = 'day') {
|
|
252
|
+
try {
|
|
253
|
+
const minuteData = await getTurnoverDataByMinute();
|
|
276
254
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
// }
|