daxiapi-cli 1.0.0 → 1.2.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 +0 -3
- package/bin/index.js +34 -12
- package/lib/api.js +142 -18
- package/lib/output.js +50 -1
- package/package.json +27 -27
package/README.md
CHANGED
package/bin/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {program} = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const config = require('../lib/config');
|
|
6
6
|
const api = require('../lib/api');
|
|
@@ -71,11 +71,10 @@ program
|
|
|
71
71
|
.option('-s, --style', '获取大小盘风格')
|
|
72
72
|
.option('-v, --value', '获取指数估值')
|
|
73
73
|
.option('-n, --news', '获取收盘新闻')
|
|
74
|
-
.
|
|
75
|
-
.action(async (options) => {
|
|
74
|
+
.action(async options => {
|
|
76
75
|
showWelcome();
|
|
77
76
|
const token = checkToken();
|
|
78
|
-
|
|
77
|
+
|
|
79
78
|
try {
|
|
80
79
|
if (options.degree) {
|
|
81
80
|
const data = await api.getMarketDegree(token);
|
|
@@ -89,9 +88,6 @@ program
|
|
|
89
88
|
} else if (options.news) {
|
|
90
89
|
const data = await api.getMarketNews(token);
|
|
91
90
|
output.newsList(data);
|
|
92
|
-
} else if (options.zdt) {
|
|
93
|
-
const data = await api.getZdtPool(token);
|
|
94
|
-
output.text(data);
|
|
95
91
|
} else {
|
|
96
92
|
// 默认获取市场概览
|
|
97
93
|
const data = await api.getMarketData(token);
|
|
@@ -109,7 +105,7 @@ program
|
|
|
109
105
|
.action(async () => {
|
|
110
106
|
showWelcome();
|
|
111
107
|
const token = checkToken();
|
|
112
|
-
|
|
108
|
+
|
|
113
109
|
try {
|
|
114
110
|
const data = await api.getIndexK(token);
|
|
115
111
|
output.klines(data);
|
|
@@ -126,10 +122,10 @@ program
|
|
|
126
122
|
.option('-o, --order <field>', '排序字段', 'cs')
|
|
127
123
|
.option('-l, --limit <days>', '天数', '5')
|
|
128
124
|
.option('-g, --gn <gnId>', '概念代码,获取概念内个股')
|
|
129
|
-
.action(async
|
|
125
|
+
.action(async options => {
|
|
130
126
|
showWelcome();
|
|
131
127
|
const token = checkToken();
|
|
132
|
-
|
|
128
|
+
|
|
133
129
|
try {
|
|
134
130
|
if (options.code) {
|
|
135
131
|
const data = await api.getSectorRankStock(token, options.code, options.order);
|
|
@@ -154,7 +150,7 @@ program
|
|
|
154
150
|
.action(async (codes, options) => {
|
|
155
151
|
showWelcome();
|
|
156
152
|
const token = checkToken();
|
|
157
|
-
|
|
153
|
+
|
|
158
154
|
try {
|
|
159
155
|
const data = await api.getStockData(token, codes.join(','));
|
|
160
156
|
if (options.json) {
|
|
@@ -175,7 +171,7 @@ program
|
|
|
175
171
|
.action(async (keyword, options) => {
|
|
176
172
|
showWelcome();
|
|
177
173
|
const token = checkToken();
|
|
178
|
-
|
|
174
|
+
|
|
179
175
|
try {
|
|
180
176
|
const data = await api.queryStock(token, keyword, options.type);
|
|
181
177
|
output.searchResult(data);
|
|
@@ -184,4 +180,30 @@ program
|
|
|
184
180
|
}
|
|
185
181
|
});
|
|
186
182
|
|
|
183
|
+
// ==================== K线数据命令 ====================
|
|
184
|
+
program
|
|
185
|
+
.command('kline <code>')
|
|
186
|
+
.description('获取个股K线数据')
|
|
187
|
+
.option('-l, --limit <days>', '数据条数', '300')
|
|
188
|
+
.option('-t, --type <type>', 'K线类型: day/week/month', 'day')
|
|
189
|
+
.option('-j, --json', '输出JSON格式')
|
|
190
|
+
.option('-s, --simple', '简单输出格式')
|
|
191
|
+
.action(async (code, options) => {
|
|
192
|
+
showWelcome();
|
|
193
|
+
// K线数据不需要token,使用免费数据源
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const data = await api.getKline(code, parseInt(options.limit), options.type);
|
|
197
|
+
if (options.json) {
|
|
198
|
+
console.log(JSON.stringify(data, null, 2));
|
|
199
|
+
} else if (options.simple) {
|
|
200
|
+
output.klineSimple(data, 10);
|
|
201
|
+
} else {
|
|
202
|
+
output.klineData(data, 20);
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
output.error(err);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
187
209
|
program.parse();
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const config = require('./config');
|
|
3
3
|
|
|
4
|
-
const BASE_URL = config.
|
|
4
|
+
const BASE_URL = config.getBaseUrl() || 'https://daxiapi.com';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取市场类型前缀
|
|
8
|
+
*/
|
|
9
|
+
function getMarketPrefix(code) {
|
|
10
|
+
if (code.startsWith('688') || code.startsWith('689')) {
|
|
11
|
+
return 'sh';
|
|
12
|
+
}
|
|
13
|
+
if (code.startsWith('60')) {
|
|
14
|
+
return 'sh';
|
|
15
|
+
}
|
|
16
|
+
return 'sz';
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
20
|
* 创建请求实例
|
|
@@ -21,7 +34,7 @@ function createClient(token) {
|
|
|
21
34
|
* GET 请求
|
|
22
35
|
*/
|
|
23
36
|
async function get(client, path) {
|
|
24
|
-
const {
|
|
37
|
+
const {data} = await client.get(path);
|
|
25
38
|
if (data.errCode !== 0) {
|
|
26
39
|
throw new Error(data.errMsg || `API Error: ${data.errCode}`);
|
|
27
40
|
}
|
|
@@ -32,7 +45,7 @@ async function get(client, path) {
|
|
|
32
45
|
* POST 请求
|
|
33
46
|
*/
|
|
34
47
|
async function post(client, path, body = {}) {
|
|
35
|
-
const {
|
|
48
|
+
const {data} = await client.post(path, body);
|
|
36
49
|
if (data.errCode !== 0) {
|
|
37
50
|
throw new Error(data.errMsg || `API Error: ${data.errCode}`);
|
|
38
51
|
}
|
|
@@ -89,20 +102,12 @@ async function getMarketNews(token) {
|
|
|
89
102
|
return get(client, '/get_market_end_news');
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
/**
|
|
93
|
-
* 获取涨跌停股票池
|
|
94
|
-
*/
|
|
95
|
-
async function getZdtPool(token) {
|
|
96
|
-
const client = createClient(token);
|
|
97
|
-
return post(client, '/get_zdt_pool');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
105
|
/**
|
|
101
106
|
* 获取板块热力图
|
|
102
107
|
*/
|
|
103
108
|
async function getSectorData(token, orderBy = 'cs', lmt = 5) {
|
|
104
109
|
const client = createClient(token);
|
|
105
|
-
return post(client, '/get_sector_data', {
|
|
110
|
+
return post(client, '/get_sector_data', {orderBy, lmt});
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
/**
|
|
@@ -110,7 +115,7 @@ async function getSectorData(token, orderBy = 'cs', lmt = 5) {
|
|
|
110
115
|
*/
|
|
111
116
|
async function getSectorRankStock(token, sectorCode, orderBy = 'cs') {
|
|
112
117
|
const client = createClient(token);
|
|
113
|
-
return post(client, '/get_sector_rank_stock', {
|
|
118
|
+
return post(client, '/get_sector_rank_stock', {sectorCode, orderBy});
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
/**
|
|
@@ -118,7 +123,7 @@ async function getSectorRankStock(token, sectorCode, orderBy = 'cs') {
|
|
|
118
123
|
*/
|
|
119
124
|
async function getGainianStock(token, gnId) {
|
|
120
125
|
const client = createClient(token);
|
|
121
|
-
return post(client, '/get_gainian_stock', {
|
|
126
|
+
return post(client, '/get_gainian_stock', {gnId});
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/**
|
|
@@ -126,7 +131,7 @@ async function getGainianStock(token, gnId) {
|
|
|
126
131
|
*/
|
|
127
132
|
async function getStockData(token, codes) {
|
|
128
133
|
const client = createClient(token);
|
|
129
|
-
return post(client, '/get_stock_data', {
|
|
134
|
+
return post(client, '/get_stock_data', {code: codes});
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
/**
|
|
@@ -134,7 +139,7 @@ async function getStockData(token, codes) {
|
|
|
134
139
|
*/
|
|
135
140
|
async function queryStock(token, keyword, type = 'stock') {
|
|
136
141
|
const client = createClient(token);
|
|
137
|
-
return post(client, '/query_stock_data', {
|
|
142
|
+
return post(client, '/query_stock_data', {q: keyword, type});
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
|
@@ -145,6 +150,123 @@ async function getGnTable(token) {
|
|
|
145
150
|
return post(client, '/get_gn_table');
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
// ==================== K线数据(多数据源) ====================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取K线数据(首选腾讯,备选东方财富)
|
|
157
|
+
* @param {string} code - 股票代码
|
|
158
|
+
* @param {number} limit - 数据条数,默认300
|
|
159
|
+
* @param {string} type - K线类型: day(日线), week(周线), month(月线)
|
|
160
|
+
* @returns {Promise<Object>} K线数据
|
|
161
|
+
*/
|
|
162
|
+
async function getKline(code, limit = 300, type = 'day') {
|
|
163
|
+
// 首选腾讯
|
|
164
|
+
try {
|
|
165
|
+
const data = await getKlineFromQQ(code, limit, type);
|
|
166
|
+
return {source: 'qq', ...data};
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.log('腾讯数据源失败,切换到东方财富...');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 备选东方财富
|
|
172
|
+
try {
|
|
173
|
+
const data = await getKlineFromEastmoney(code, limit, type);
|
|
174
|
+
return {source: 'eastmoney', ...data};
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new Error('所有数据源均失败,请稍后重试');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 腾讯财经K线数据
|
|
182
|
+
* @param {string} code - 股票代码
|
|
183
|
+
* @param {number} limit - 数据条数
|
|
184
|
+
* @param {string} type - K线类型
|
|
185
|
+
*/
|
|
186
|
+
async function getKlineFromQQ(code, limit = 500, type = 'day') {
|
|
187
|
+
const market = getMarketPrefix(code);
|
|
188
|
+
const fullCode = `${market}${code}`;
|
|
189
|
+
|
|
190
|
+
// 腾讯接口限制最大2000条
|
|
191
|
+
if (limit > 2000) {
|
|
192
|
+
limit = 2000;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const url = `https://proxy.finance.qq.com/ifzqgtimg/appstock/app/newfqkline/get?_var=¶m=${fullCode},${type},,,${limit},qfq&r=0.${Date.now()}`;
|
|
196
|
+
|
|
197
|
+
const {data} = await axios.get(url, {timeout: 10000});
|
|
198
|
+
|
|
199
|
+
if (!data.data || !data.data[fullCode]) {
|
|
200
|
+
throw new Error('腾讯数据源返回异常');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const klineData = data.data[fullCode].qfqday || data.data[fullCode].day;
|
|
204
|
+
if (!klineData || klineData.length === 0) {
|
|
205
|
+
throw new Error('无K线数据');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const klines = klineData.map(([date, open, close, high, low, volume, , , amount]) => ({
|
|
209
|
+
date,
|
|
210
|
+
open: parseFloat(open),
|
|
211
|
+
close: parseFloat(close),
|
|
212
|
+
high: parseFloat(high),
|
|
213
|
+
low: parseFloat(low),
|
|
214
|
+
volume: parseFloat(volume),
|
|
215
|
+
amount: parseFloat(amount)
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
code,
|
|
220
|
+
name: data.data[fullCode].qt?.[fullCode]?.[1] || code,
|
|
221
|
+
klines,
|
|
222
|
+
count: klines.length
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 东方财富K线数据
|
|
228
|
+
* @param {string} code - 股票代码
|
|
229
|
+
* @param {number} limit - 数据条数
|
|
230
|
+
* @param {string} type - K线类型: day=101, week=102, month=103
|
|
231
|
+
*/
|
|
232
|
+
async function getKlineFromEastmoney(code, limit = 300, type = 'day') {
|
|
233
|
+
// K线类型映射
|
|
234
|
+
const kltMap = {day: '101', week: '102', month: '103'};
|
|
235
|
+
const klt = kltMap[type] || '101';
|
|
236
|
+
|
|
237
|
+
// 市场代码
|
|
238
|
+
const secid = code.startsWith('6') ? `1.${code}` : `0.${code}`;
|
|
239
|
+
|
|
240
|
+
const url = `https://push2his.eastmoney.com/api/qt/stock/kline/get?fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f61&ut=7eea3edcaed734bea9cbfc24409ed989&end=29991010&klt=${klt}&secid=${secid}&fqt=1&lmt=${limit}`;
|
|
241
|
+
|
|
242
|
+
const {data} = await axios.get(url, {timeout: 10000});
|
|
243
|
+
|
|
244
|
+
if (!data.data || !data.data.klines) {
|
|
245
|
+
throw new Error('东方财富数据源返回异常');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const klines = data.data.klines.map(line => {
|
|
249
|
+
const [date, open, close, high, low, volume, amount, , turnover] = line.split(',');
|
|
250
|
+
return {
|
|
251
|
+
date,
|
|
252
|
+
open: parseFloat(open),
|
|
253
|
+
close: parseFloat(close),
|
|
254
|
+
high: parseFloat(high),
|
|
255
|
+
low: parseFloat(low),
|
|
256
|
+
volume: parseFloat(volume),
|
|
257
|
+
amount: parseFloat(amount),
|
|
258
|
+
turnover: parseFloat(turnover) || 0
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
code,
|
|
264
|
+
name: data.data.name || code,
|
|
265
|
+
klines,
|
|
266
|
+
count: klines.length
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
148
270
|
module.exports = {
|
|
149
271
|
getIndexK,
|
|
150
272
|
getMarketData,
|
|
@@ -152,11 +274,13 @@ module.exports = {
|
|
|
152
274
|
getMarketStyle,
|
|
153
275
|
getMarketValueData,
|
|
154
276
|
getMarketNews,
|
|
155
|
-
getZdtPool,
|
|
156
277
|
getSectorData,
|
|
157
278
|
getSectorRankStock,
|
|
158
279
|
getGainianStock,
|
|
159
280
|
getStockData,
|
|
160
281
|
queryStock,
|
|
161
|
-
getGnTable
|
|
282
|
+
getGnTable,
|
|
283
|
+
getKline,
|
|
284
|
+
getKlineFromQQ,
|
|
285
|
+
getKlineFromEastmoney
|
|
162
286
|
};
|
package/lib/output.js
CHANGED
|
@@ -235,6 +235,46 @@ function searchResult(data) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* K线数据输出(增强版)
|
|
240
|
+
*/
|
|
241
|
+
function klineData(data, limit = 20) {
|
|
242
|
+
console.log(chalk.bold.cyan(`📊 ${data.name} (${data.code}) K线数据\n`));
|
|
243
|
+
console.log(chalk.gray(`数据源: ${data.source === 'qq' ? '腾讯财经' : '东方财富'}`));
|
|
244
|
+
console.log(chalk.gray(`数据条数: ${data.count}\n`));
|
|
245
|
+
|
|
246
|
+
if (data.klines && data.klines.length > 0) {
|
|
247
|
+
const displayData = data.klines.slice(-limit);
|
|
248
|
+
const tableData = displayData.map(item => ({
|
|
249
|
+
'日期': item.date,
|
|
250
|
+
'开盘': item.open?.toFixed(2),
|
|
251
|
+
'收盘': item.close?.toFixed(2),
|
|
252
|
+
'最高': item.high?.toFixed(2),
|
|
253
|
+
'最低': item.low?.toFixed(2),
|
|
254
|
+
'涨跌幅': formatPercent(item.close && item.open ? ((item.close - item.open) / item.open * 100) : null),
|
|
255
|
+
'成交量': formatVolume(item.volume),
|
|
256
|
+
'成交额': formatAmount(item.amount)
|
|
257
|
+
}));
|
|
258
|
+
console.table(tableData);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* K线简单输出(最近N天)
|
|
264
|
+
*/
|
|
265
|
+
function klineSimple(data, limit = 10) {
|
|
266
|
+
if (data.klines && data.klines.length > 0) {
|
|
267
|
+
const displayData = data.klines.slice(-limit);
|
|
268
|
+
console.log(chalk.bold(`${data.name} (${data.code}) 最近${displayData.length}日K线 [${data.source}]:`));
|
|
269
|
+
|
|
270
|
+
displayData.forEach(item => {
|
|
271
|
+
const change = item.close && item.open ? ((item.close - item.open) / item.open * 100) : 0;
|
|
272
|
+
const changeStr = change >= 0 ? chalk.red(`+${change.toFixed(2)}%`) : chalk.green(`${change.toFixed(2)}%`);
|
|
273
|
+
console.log(` ${item.date} | 开${item.open?.toFixed(2)} 收${item.close?.toFixed(2)} 高${item.high?.toFixed(2)} 低${item.low?.toFixed(2)} | ${changeStr}`);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
238
278
|
// ==================== 辅助函数 ====================
|
|
239
279
|
|
|
240
280
|
function formatPercent(value) {
|
|
@@ -264,6 +304,13 @@ function formatVolume(vol) {
|
|
|
264
304
|
return vol.toString();
|
|
265
305
|
}
|
|
266
306
|
|
|
307
|
+
function formatAmount(amount) {
|
|
308
|
+
if (!amount) return '-';
|
|
309
|
+
if (amount >= 100000000) return (amount / 100000000).toFixed(2) + '亿';
|
|
310
|
+
if (amount >= 10000) return (amount / 10000).toFixed(2) + '万';
|
|
311
|
+
return amount.toFixed(2);
|
|
312
|
+
}
|
|
313
|
+
|
|
267
314
|
module.exports = {
|
|
268
315
|
error,
|
|
269
316
|
text,
|
|
@@ -275,5 +322,7 @@ module.exports = {
|
|
|
275
322
|
sectorHeatmap,
|
|
276
323
|
stockList,
|
|
277
324
|
stockDetail,
|
|
278
|
-
searchResult
|
|
325
|
+
searchResult,
|
|
326
|
+
klineData,
|
|
327
|
+
klineSimple
|
|
279
328
|
};
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
2
|
+
"name": "daxiapi-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "大虾皮(daxiapi.com)金融数据API命令行工具",
|
|
5
|
+
"bin": {
|
|
6
|
+
"daxiapi": "./bin/index.js",
|
|
7
|
+
"dxp": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"stock",
|
|
14
|
+
"api",
|
|
15
|
+
"daxiapi",
|
|
16
|
+
"cli",
|
|
17
|
+
"finance"
|
|
18
|
+
],
|
|
19
|
+
"author": "daxiapi.com",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"axios": "^1.6.0",
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"commander": "^11.1.0",
|
|
25
|
+
"conf": "^10.2.0",
|
|
26
|
+
"console.table": "^0.10.0"
|
|
27
|
+
}
|
|
28
|
+
}
|