daxiapi-cli 1.0.0 → 1.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/bin/index.js +26 -0
- package/lib/api.js +146 -1
- package/lib/output.js +50 -1
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -184,4 +184,30 @@ program
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
// ==================== K线数据命令 ====================
|
|
188
|
+
program
|
|
189
|
+
.command('kline <code>')
|
|
190
|
+
.description('获取个股K线数据')
|
|
191
|
+
.option('-l, --limit <days>', '数据条数', '300')
|
|
192
|
+
.option('-t, --type <type>', 'K线类型: day/week/month', 'day')
|
|
193
|
+
.option('-j, --json', '输出JSON格式')
|
|
194
|
+
.option('-s, --simple', '简单输出格式')
|
|
195
|
+
.action(async (code, options) => {
|
|
196
|
+
showWelcome();
|
|
197
|
+
// K线数据不需要token,使用免费数据源
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const data = await api.getKline(code, parseInt(options.limit), options.type);
|
|
201
|
+
if (options.json) {
|
|
202
|
+
console.log(JSON.stringify(data, null, 2));
|
|
203
|
+
} else if (options.simple) {
|
|
204
|
+
output.klineSimple(data, 10);
|
|
205
|
+
} else {
|
|
206
|
+
output.klineData(data, 20);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
output.error(err);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
187
213
|
program.parse();
|
package/lib/api.js
CHANGED
|
@@ -3,6 +3,25 @@ const config = require('./config');
|
|
|
3
3
|
|
|
4
4
|
const BASE_URL = config.get('baseUrl') || 'https://daxiapi.com';
|
|
5
5
|
|
|
6
|
+
// 市场类型映射
|
|
7
|
+
const MARKET_MAP = {
|
|
8
|
+
'6': 'sh', // 上海
|
|
9
|
+
'0': 'sz', // 深圳
|
|
10
|
+
'3': 'sz', // 创业板
|
|
11
|
+
'9': 'bj',
|
|
12
|
+
'688': 'sh', // 科创板
|
|
13
|
+
'689': 'sh' // 科创板CDR
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 获取市场类型前缀
|
|
18
|
+
*/
|
|
19
|
+
function getMarketPrefix(code) {
|
|
20
|
+
if (code.startsWith('688') || code.startsWith('689')) return 'sh';
|
|
21
|
+
if (code.startsWith('60')) return 'sh';
|
|
22
|
+
return 'sz';
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* 创建请求实例
|
|
8
27
|
*/
|
|
@@ -145,6 +164,129 @@ async function getGnTable(token) {
|
|
|
145
164
|
return post(client, '/get_gn_table');
|
|
146
165
|
}
|
|
147
166
|
|
|
167
|
+
// ==================== K线数据(多数据源) ====================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 获取K线数据(首选腾讯,备选东方财富)
|
|
171
|
+
* @param {string} code - 股票代码
|
|
172
|
+
* @param {number} limit - 数据条数,默认300
|
|
173
|
+
* @param {string} type - K线类型: day(日线), week(周线), month(月线)
|
|
174
|
+
* @returns {Promise<Object>} K线数据
|
|
175
|
+
*/
|
|
176
|
+
async function getKline(code, limit = 300, type = 'day') {
|
|
177
|
+
// 首选腾讯
|
|
178
|
+
try {
|
|
179
|
+
const data = await getKlineFromQQ(code, limit, type);
|
|
180
|
+
return { source: 'qq', ...data };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.log('腾讯数据源失败,切换到东方财富...');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 备选东方财富
|
|
186
|
+
try {
|
|
187
|
+
const data = await getKlineFromEastmoney(code, limit, type);
|
|
188
|
+
return { source: 'eastmoney', ...data };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
throw new Error('所有数据源均失败,请稍后重试');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 腾讯财经K线数据
|
|
196
|
+
* @param {string} code - 股票代码
|
|
197
|
+
* @param {number} limit - 数据条数
|
|
198
|
+
* @param {string} type - K线类型
|
|
199
|
+
*/
|
|
200
|
+
async function getKlineFromQQ(code, limit = 500, type = 'day') {
|
|
201
|
+
const market = getMarketPrefix(code);
|
|
202
|
+
const fullCode = `${market}${code}`;
|
|
203
|
+
|
|
204
|
+
// 腾讯接口限制最大2000条
|
|
205
|
+
if (limit > 2000) limit = 2000;
|
|
206
|
+
|
|
207
|
+
const url = `https://proxy.finance.qq.com/ifzqgtimg/appstock/app/newfqkline/get?_var=¶m=${fullCode},${type},,,${limit},qfq&r=0.${Date.now()}`;
|
|
208
|
+
|
|
209
|
+
const { data } = await axios.get(url, { timeout: 10000 });
|
|
210
|
+
|
|
211
|
+
if (!data.data || !data.data[fullCode]) {
|
|
212
|
+
throw new Error('腾讯数据源返回异常');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const klineData = data.data[fullCode].qfqday || data.data[fullCode].day;
|
|
216
|
+
if (!klineData || klineData.length === 0) {
|
|
217
|
+
throw new Error('无K线数据');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const klines = klineData.map(([date, open, close, high, low, volume, , , amount]) => ({
|
|
221
|
+
date,
|
|
222
|
+
open: parseFloat(open),
|
|
223
|
+
close: parseFloat(close),
|
|
224
|
+
high: parseFloat(high),
|
|
225
|
+
low: parseFloat(low),
|
|
226
|
+
volume: parseFloat(volume),
|
|
227
|
+
amount: parseFloat(amount)
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
code,
|
|
232
|
+
name: data.data[fullCode].qt?.[fullCode]?.[1] || code,
|
|
233
|
+
klines,
|
|
234
|
+
count: klines.length
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 东方财富K线数据
|
|
240
|
+
* @param {string} code - 股票代码
|
|
241
|
+
* @param {number} limit - 数据条数
|
|
242
|
+
* @param {string} type - K线类型: day=101, week=102, month=103
|
|
243
|
+
*/
|
|
244
|
+
async function getKlineFromEastmoney(code, limit = 300, type = 'day') {
|
|
245
|
+
// K线类型映射
|
|
246
|
+
const kltMap = { day: '101', week: '102', month: '103' };
|
|
247
|
+
const klt = kltMap[type] || '101';
|
|
248
|
+
|
|
249
|
+
// 市场代码
|
|
250
|
+
const secid = code.startsWith('6') ? `1.${code}` : `0.${code}`;
|
|
251
|
+
|
|
252
|
+
const url = `https://push2his.eastmoney.com/api/qt/stock/kline/get?` +
|
|
253
|
+
`fields1=f1,f2,f3,f4,f5,f6&` +
|
|
254
|
+
`fields2=f51,f52,f53,f54,f55,f56,f57,f58,f61&` +
|
|
255
|
+
`ut=7eea3edcaed734bea9cbfc24409ed989&` +
|
|
256
|
+
`end=29991010&` +
|
|
257
|
+
`klt=${klt}&` +
|
|
258
|
+
`secid=${secid}&` +
|
|
259
|
+
`fqt=1&` +
|
|
260
|
+
`lmt=${limit}`;
|
|
261
|
+
|
|
262
|
+
const { data } = await axios.get(url, { timeout: 10000 });
|
|
263
|
+
|
|
264
|
+
if (!data.data || !data.data.klines) {
|
|
265
|
+
throw new Error('东方财富数据源返回异常');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const klines = data.data.klines.map(line => {
|
|
269
|
+
const [date, open, close, high, low, volume, amount, , turnover] = line.split(',');
|
|
270
|
+
return {
|
|
271
|
+
date,
|
|
272
|
+
open: parseFloat(open),
|
|
273
|
+
close: parseFloat(close),
|
|
274
|
+
high: parseFloat(high),
|
|
275
|
+
low: parseFloat(low),
|
|
276
|
+
volume: parseFloat(volume),
|
|
277
|
+
amount: parseFloat(amount),
|
|
278
|
+
turnover: parseFloat(turnover) || 0
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
code,
|
|
284
|
+
name: data.data.name || code,
|
|
285
|
+
klines,
|
|
286
|
+
count: klines.length
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
148
290
|
module.exports = {
|
|
149
291
|
getIndexK,
|
|
150
292
|
getMarketData,
|
|
@@ -158,5 +300,8 @@ module.exports = {
|
|
|
158
300
|
getGainianStock,
|
|
159
301
|
getStockData,
|
|
160
302
|
queryStock,
|
|
161
|
-
getGnTable
|
|
303
|
+
getGnTable,
|
|
304
|
+
getKline,
|
|
305
|
+
getKlineFromQQ,
|
|
306
|
+
getKlineFromEastmoney
|
|
162
307
|
};
|
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
|
};
|