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 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=&param=${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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daxiapi-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "大虾皮(daxiapi.com)金融数据API命令行工具",
5
5
  "bin": {
6
6
  "daxiapi": "./bin/index.js",