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 CHANGED
@@ -59,9 +59,6 @@ daxiapi market -v
59
59
 
60
60
  # 收盘新闻
61
61
  daxiapi market -n
62
-
63
- # 涨跌停股票池
64
- daxiapi market -z
65
62
  ```
66
63
 
67
64
  ### 指数K线
package/bin/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
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
- .option('-z, --zdt', '获取涨跌停股票池')
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 (options) => {
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.get('baseUrl') || 'https://daxiapi.com';
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 { data } = await client.get(path);
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 { data } = await client.post(path, body);
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', { orderBy, lmt });
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', { sectorCode, orderBy });
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', { gnId });
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', { code: codes });
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', { q: keyword, type });
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=&param=${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
- "name": "daxiapi-cli",
3
- "version": "1.0.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
- }
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
+ }