stock-kline-cli
Version:
在终端上打印股票实时数据和K线图的命令行工具
388 lines (328 loc) • 12.1 kB
JavaScript
const axios = require('axios');
const iconv = require('iconv-lite');
const { table } = require('table');
const fs = require('fs');
const { program } = require('commander');
const asciichart = require('asciichart');
const chalk = require('chalk');
const columns = ['名字', '代码', '当前股价', '今日涨跌幅', '昨日收盘价'];
const config = {
showHead: true,
defaultPeriod: 20, // 默认周期数
enableAIAnalysis: false // 默认不开启AI分析
}
// 股票市场配置
const MARKET_CONFIG = {
// A股
sh: { type: 'A', name: '上证', api: 'qt.gtimg.cn', needConvert: true, currency: '¥' },
sz: { type: 'A', name: '深证', api: 'qt.gtimg.cn', needConvert: true, currency: '¥' },
// 港股
hk: { type: 'HK', name: '港股', api: 'qt.gtimg.cn', needConvert: true, currency: 'HK$' },
// 美股
us: { type: 'US', name: '美股', api: 'qt.gtimg.cn', needConvert: true, currency: '$' }
};
program
.version('1.0.4', '-v, --version')
.option('-c, --config <path>', '设置配置文件')
.option('-d', '隐藏表头')
.option('-s, --stock <list>', '设置stock代码, 多个以逗号隔开\n A股示例: sh600000,sz000001\n 港股示例: hk00700\n 美股示例: usAAPL', (val) => val.split(','))
.option('--day', '显示日K数据')
.option('--week', '显示周K数据')
.option('-p, --period <number>', '设置显示的周期数量,默认20', (val) => parseInt(val))
.option('--height <number>', '设置图表高度,默认15', (val) => parseInt(val))
.option('--ai', '开启AI分析')
.helpOption('--help', '显示帮助信息')
program.parse();
const options = program.opts();
if (options.d) {
config.showHead = false;
}
if (options.ai) {
config.enableAIAnalysis = true;
}
if (options.stock) {
printStock(options.stock)
} else if (options.config) {
const data = fs.readFileSync(options.config);
const configData = JSON.parse(data);
printStock(configData.stocks);
}
// 获取股票市场信息
function getMarketInfo(code) {
const market = code.substring(0, 2).toLowerCase();
return MARKET_CONFIG[market] || null;
}
async function getKLineData(code, type = 'day') {
try {
const marketInfo = getMarketInfo(code);
if (!marketInfo) {
throw new Error(`不支持的股票代码格式: ${code}`);
}
if (marketInfo.type === 'US') {
console.log(chalk.red(`\n美股暂不支持K线图: ${code}`));
return []
}
const klineType = type === 'day' ? 'day' : 'week';
const period = options.period || config.defaultPeriod;
// 根据不同市场使用不同的API参数
let apiUrl;
if (marketInfo.type === 'HK') {
apiUrl = `https://web.ifzq.gtimg.cn/appstock/app/kline/kline?_var=kline_${klineType}${code}¶m=${code},${klineType},,,${period}`;
} else if (marketInfo.type === 'A') {
apiUrl = `https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${code},${klineType},,,${period},qfq`;
}
// console.log('正在获取K线数据:', apiUrl);
const response = await axios({
method: 'get',
url: apiUrl,
headers: {
'Referer': 'http://web.ifzq.gtimg.cn/',
'User-Agent': 'Mozilla/5.0'
}
});
let data = response.data;
// 处理港股的数据格式
if (marketInfo.type === 'HK') {
if (typeof data === 'string') {
data = JSON.parse(data.replace(/^[^{]*=/, ''));
}
}
const klineData = [];
let stockData = data.data[code];
if (!stockData) {
console.error(`未能获取到${code}的K线数据`);
return [];
}
// console.log('数据结构:', Object.keys(stockData));
// console.log('K线类型:', klineType);
// 不同市场和类型的数据结构不同
let klines = stockData[klineType] || stockData[`qfq${klineType}`];
if (!klines) {
console.error(`未能获取到${code}的${type}K线数据`);
return [];
}
// console.log('K线数据长度:', klines.length);
// if (klines.length > 0) {
// console.log('第一条数据:', klines[0]);
// }
klines.forEach(k => {
klineData.push({
date: k[0],
open: parseFloat(k[1]),
close: parseFloat(k[2]),
high: parseFloat(k[3]),
low: parseFloat(k[4]),
volume: parseFloat(k[5])
});
});
return klineData;
} catch (error) {
console.error(`获取K线数据失败: ${error.message}`);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
return [];
}
}
// 分析K线形态
function analyzeKLinePattern(klineData) {
if (klineData.length < 3) return [];
const patterns = [];
// 分析最近三天的K线
const recent = klineData.slice(-3);
// 判断锤子线
const last = recent[2];
const bodyLength = Math.abs(last.close - last.open);
const shadowLength = last.high - last.low;
const lowerShadow = Math.min(last.open, last.close) - last.low;
if (lowerShadow > bodyLength * 2 && shadowLength > bodyLength * 3) {
patterns.push({
type: '锤子线',
position: last.date,
meaning: '可能预示着下跌趋势即将结束,市场可能反转向上'
});
}
// 判断启明星形态
if (recent.length === 3) {
const [day1, day2, day3] = recent;
const day1Body = day1.close - day1.open;
const day2Body = Math.abs(day2.close - day2.open);
const day3Body = day3.close - day3.open;
if (day1Body < 0 && // 第一天下跌
day2Body < Math.abs(day1Body) * 0.3 && // 第二天十字星
day3Body > 0 && // 第三天上涨
day2.high < day1.close && // 缺口
day3.open > day2.high) {
patterns.push({
type: '启明星',
position: day3.date,
meaning: '强势反转信号,预示着可能开始上涨趋势'
});
}
}
return patterns;
}
// 分析趋势
function analyzeTrend(klineData) {
if (klineData.length < 5) return null;
const prices = klineData.map(k => k.close);
const ma5 = prices.slice(-5).reduce((a, b) => a + b, 0) / 5;
const ma10 = klineData.length >= 10 ?
prices.slice(-10).reduce((a, b) => a + b, 0) / 10 : null;
const lastPrice = prices[prices.length - 1];
const startPrice = prices[0];
const priceChange = ((lastPrice - startPrice) / startPrice * 100).toFixed(2);
let trend = '';
let strength = '';
// 判断趋势
if (lastPrice > ma5 && (ma10 === null || ma5 > ma10)) {
trend = '上涨';
strength = priceChange > 5 ? '强势' : '弱势';
} else if (lastPrice < ma5 && (ma10 === null || ma5 < ma10)) {
trend = '下跌';
strength = priceChange < -5 ? '强势' : '弱势';
} else {
trend = '盘整';
strength = '震荡';
}
return {
trend,
strength,
priceChange: `${priceChange}%`,
analysis: `${strength}${trend}趋势,区间涨跌幅${priceChange}%,${
trend === '盘整' ? '建议观望' :
trend === '上涨' ? (strength === '强势' ? '注意防守' : '可以跟进') :
strength === '强势' ? '注意止损' : '等待企稳'
}`
};
}
function printKLineChart(klineData, type, stockName, code) {
if (klineData.length === 0) return;
const prices = klineData.map(item => item.close);
const highs = klineData.map(item => item.high);
const lows = klineData.map(item => item.low);
const dates = klineData.map(item => item.date);
// 设置图表配置
const chartConfig = {
height: options.height || 15,
colors: [
asciichart.blue,
asciichart.green,
asciichart.red
]
};
// 创建多线图表
console.log(chalk.yellow(`\n${stockName}(${code}) ${type}K线图 (${klineData.length}个周期):`));
console.log(chalk.yellow('价格走势 (蓝色:收盘价 绿色:最高价 红色:最低价)'));
console.log(asciichart.plot([prices, highs, lows], chartConfig));
// 显示日期范围
console.log(chalk.cyan('\n时间范围:'));
console.log(`${dates[0]} 至 ${dates[dates.length - 1]}`);
// 显示统计信息
const maxPrice = Math.max(...highs);
const minPrice = Math.min(...lows);
const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
const priceChange = ((prices[prices.length - 1] - prices[0]) / prices[0] * 100).toFixed(2);
console.log(chalk.cyan('\n价格统计:'));
console.log(chalk.green(`最高: ${maxPrice.toFixed(2)}`));
console.log(chalk.red(`最低: ${minPrice.toFixed(2)}`));
console.log(chalk.yellow(`平均: ${avgPrice.toFixed(2)}`));
console.log(chalk.cyan(`区间涨跌: ${priceChange}%`));
// 只在启用AI分析时显示分析结果
if (config.enableAIAnalysis) {
// 添加AI分析
console.log(chalk.magenta('\nAI分析:'));
// 分析K线形态
const patterns = analyzeKLinePattern(klineData);
if (patterns.length > 0) {
console.log(chalk.yellow('发现K线形态:'));
patterns.forEach(p => {
console.log(`- ${p.type} (${p.position})`);
console.log(` ${p.meaning}`);
});
}
// 分析趋势
const trend = analyzeTrend(klineData);
if (trend) {
console.log(chalk.yellow('\n趋势分析:'));
console.log(`- 当前趋势: ${trend.trend} (${trend.strength})`);
console.log(`- 分析建议: ${trend.analysis}`);
}
}
}
async function printStock(list) {
try {
const options = program.opts();
// 按市场分组股票代码
const stocksByMarket = list.reduce((acc, code) => {
const marketInfo = getMarketInfo(code);
if (!marketInfo) {
console.error(`不支持的股票代码格式: ${code}`);
return acc;
}
if (!acc[marketInfo.type]) {
acc[marketInfo.type] = [];
}
acc[marketInfo.type].push(code);
return acc;
}, {});
let tableData = [];
if (config.showHead) {
tableData.push(['名字', '代码', '当前股价', '今日涨跌幅', '昨日收盘价']);
}
// 处理每个市场的股票
for (const [marketType, stocks] of Object.entries(stocksByMarket)) {
const response = await axios({
method: 'get',
url: `https://qt.gtimg.cn/q=${stocks.join(',')}`,
responseType: "arraybuffer"
});
let data;
if (marketType === 'US') {
data = iconv.decode(response.data, 'gbk');
} else {
data = iconv.decode(response.data, 'gbk');
}
let arr = data.split('\n');
for (let i = 0; i < arr.length - 1; i++) {
let item = arr[i];
let match = item.match(/v_(.+)="(.+)"/);
if (match) {
let values = match[2].split('~');
let stockCode = match[1];
let stockName = values[1];
let marketInfo = getMarketInfo(stockCode);
// 根据不同市场调整数据位置
const priceIndex = marketType === 'US' ? 3 : 3;
const changeIndex = marketType === 'US' ? 32 : 32;
const prevCloseIndex = marketType === 'US' ? 4 : 4;
// 添加币种符号
const price = `${marketInfo.currency}${values[priceIndex]}`;
const prevClose = `${marketInfo.currency}${values[prevCloseIndex]}`;
tableData.push([
`${stockName}(${marketInfo.name})`,
stockCode,
price,
values[changeIndex] + '%',
prevClose
]);
// 如果开启了日K或周K选项,获取并打印K线图表
if (options.day) {
const dayKData = await getKLineData(stockCode, 'day');
printKLineChart(dayKData, '日', stockName, stockCode);
}
if (options.week) {
const weekKData = await getKLineData(stockCode, 'week');
printKLineChart(weekKData, '周', stockName, stockCode);
}
}
}
}
console.log('\n实时行情:');
console.log(table(tableData));
} catch (error) {
console.error('获取数据失败:', error.message);
}
}