|
|
| """
|
| 智能分析系统(股票) - 股票市场数据分析系统
|
| 开发者:熊猫大侠
|
| 版本:v2.1.0
|
| 许可证:MIT License
|
| """
|
|
|
| import logging
|
| import random
|
| import akshare as ak
|
| import pandas as pd
|
| import numpy as np
|
| from datetime import datetime, timedelta
|
|
|
|
|
| class IndustryAnalyzer:
|
| def __init__(self):
|
| """初始化行业分析类"""
|
| self.data_cache = {}
|
| self.industry_code_map = {}
|
|
|
|
|
| logging.basicConfig(level=logging.INFO,
|
| format='%(asctime)s - %(levelname)s - %(message)s')
|
| self.logger = logging.getLogger(__name__)
|
|
|
| def get_industry_fund_flow(self, symbol="即时"):
|
| """获取行业资金流向数据"""
|
| try:
|
|
|
| cache_key = f"industry_fund_flow_{symbol}"
|
|
|
|
|
| if cache_key in self.data_cache:
|
| cache_time, cached_data = self.data_cache[cache_key]
|
|
|
| if (datetime.now() - cache_time).total_seconds() < 1800:
|
| self.logger.info(f"从缓存获取行业资金流向数据: {symbol}")
|
| return cached_data
|
|
|
|
|
| self.logger.info(f"从API获取行业资金流向数据: {symbol}")
|
| fund_flow_data = ak.stock_fund_flow_industry(symbol=symbol)
|
|
|
|
|
| self.logger.info(f"行业资金流向数据列名: {fund_flow_data.columns.tolist()}")
|
|
|
|
|
| result = []
|
|
|
| if symbol == "即时":
|
| for _, row in fund_flow_data.iterrows():
|
| try:
|
|
|
| item = {
|
| "rank": self._safe_int(row["序号"]),
|
| "industry": str(row["行业"]),
|
| "index": self._safe_float(row["行业指数"]),
|
| "change": self._safe_percent(row["行业-涨跌幅"]),
|
| "inflow": self._safe_float(row["流入资金"]),
|
| "outflow": self._safe_float(row["流出资金"]),
|
| "netFlow": self._safe_float(row["净额"]),
|
| "companyCount": self._safe_int(row["公司家数"])
|
| }
|
|
|
|
|
| if "领涨股" in row:
|
| item["leadingStock"] = str(row["领涨股"])
|
| if "领涨股-涨跌幅" in row:
|
| item["leadingStockChange"] = self._safe_percent(row["领涨股-涨跌幅"])
|
| if "当前价" in row:
|
| item["leadingStockPrice"] = self._safe_float(row["当前价"])
|
|
|
| result.append(item)
|
| except Exception as e:
|
| self.logger.warning(f"处理行业资金流向数据行时出错: {str(e)}")
|
| continue
|
| else:
|
| for _, row in fund_flow_data.iterrows():
|
| try:
|
| item = {
|
| "rank": self._safe_int(row["序号"]),
|
| "industry": str(row["行业"]),
|
| "companyCount": self._safe_int(row["公司家数"]),
|
| "index": self._safe_float(row["行业指数"]),
|
| "change": self._safe_percent(row["阶段涨跌幅"]),
|
| "inflow": self._safe_float(row["流入资金"]),
|
| "outflow": self._safe_float(row["流出资金"]),
|
| "netFlow": self._safe_float(row["净额"])
|
| }
|
| result.append(item)
|
| except Exception as e:
|
| self.logger.warning(f"处理行业资金流向数据行时出错: {str(e)}")
|
| continue
|
|
|
|
|
| self.data_cache[cache_key] = (datetime.now(), result)
|
|
|
| return result
|
|
|
| except Exception as e:
|
| self.logger.error(f"获取行业资金流向数据失败: {str(e)}")
|
|
|
| import traceback
|
| self.logger.error(traceback.format_exc())
|
| return []
|
|
|
| def _safe_float(self, value):
|
| """安全地将值转换为浮点数"""
|
| try:
|
| if pd.isna(value):
|
| return 0.0
|
| return float(value)
|
| except:
|
| return 0.0
|
|
|
| def _safe_int(self, value):
|
| """安全地将值转换为整数"""
|
| try:
|
| if pd.isna(value):
|
| return 0
|
| return int(value)
|
| except:
|
| return 0
|
|
|
| def _safe_percent(self, value):
|
| """安全地将百分比值转换为字符串格式"""
|
| try:
|
| if pd.isna(value):
|
| return "0.00"
|
|
|
|
|
| if isinstance(value, str) and "%" in value:
|
| return value.replace("%", "")
|
|
|
|
|
| return str(float(value))
|
| except:
|
| return "0.00"
|
|
|
| def _get_industry_code(self, industry_name):
|
| """获取行业名称对应的板块代码"""
|
| try:
|
|
|
| if not self.industry_code_map:
|
|
|
| industry_list = ak.stock_board_industry_name_em()
|
|
|
|
|
| for _, row in industry_list.iterrows():
|
| if '板块名称' in industry_list.columns and '板块代码' in industry_list.columns:
|
| name = row['板块名称']
|
| code = row['板块代码']
|
| self.industry_code_map[name] = code
|
|
|
| self.logger.info(f"成功获取到 {len(self.industry_code_map)} 个行业代码映射")
|
|
|
|
|
| if industry_name in self.industry_code_map:
|
| return self.industry_code_map[industry_name]
|
|
|
|
|
| for name, code in self.industry_code_map.items():
|
| if industry_name in name or name in industry_name:
|
| self.logger.info(f"行业名称 '{industry_name}' 模糊匹配到 '{name}',代码: {code}")
|
| return code
|
|
|
|
|
| self.logger.warning(f"未找到行业 '{industry_name}' 对应的代码")
|
| return None
|
|
|
| except Exception as e:
|
| self.logger.error(f"获取行业代码时出错: {str(e)}")
|
| import traceback
|
| self.logger.error(traceback.format_exc())
|
| return None
|
|
|
| def get_industry_stocks(self, industry):
|
| """获取行业成分股"""
|
| try:
|
|
|
| cache_key = f"industry_stocks_{industry}"
|
|
|
|
|
| if cache_key in self.data_cache:
|
| cache_time, cached_data = self.data_cache[cache_key]
|
|
|
| if (datetime.now() - cache_time).total_seconds() < 3600:
|
| self.logger.info(f"从缓存获取行业成分股: {industry}")
|
| return cached_data
|
|
|
|
|
| self.logger.info(f"获取 {industry} 行业成分股")
|
|
|
| result = []
|
| try:
|
|
|
| try:
|
| stocks = ak.stock_board_industry_cons_em(symbol=industry)
|
| self.logger.info(f"使用行业名称 '{industry}' 成功获取成分股")
|
| except Exception as direct_error:
|
| self.logger.warning(f"使用行业名称获取成分股失败: {str(direct_error)}")
|
|
|
| industry_code = self._get_industry_code(industry)
|
| if industry_code:
|
| self.logger.info(f"尝试使用行业代码 {industry_code} 获取成分股")
|
| stocks = ak.stock_board_industry_cons_em(symbol=industry_code)
|
| else:
|
|
|
| raise ValueError(f"无法找到行业 '{industry}' 对应的代码")
|
|
|
|
|
| self.logger.info(f"行业成分股数据列名: {stocks.columns.tolist()}")
|
|
|
|
|
| if not stocks.empty:
|
| for _, row in stocks.iterrows():
|
| try:
|
| item = {
|
| "code": str(row["代码"]),
|
| "name": str(row["名称"]),
|
| "price": self._safe_float(row["最新价"]),
|
| "change": self._safe_float(row["涨跌幅"]),
|
| "change_amount": self._safe_float(row["涨跌额"]) if "涨跌额" in row else 0.0,
|
| "volume": self._safe_float(row["成交量"]) if "成交量" in row else 0.0,
|
| "turnover": self._safe_float(row["成交额"]) if "成交额" in row else 0.0,
|
| "amplitude": self._safe_float(row["振幅"]) if "振幅" in row else 0.0,
|
| "turnover_rate": self._safe_float(row["换手率"]) if "换手率" in row else 0.0
|
| }
|
| result.append(item)
|
| except Exception as e:
|
| self.logger.warning(f"处理行业成分股数据行时出错: {str(e)}")
|
| continue
|
|
|
| except Exception as e:
|
|
|
| self.logger.warning(f"无法通过API获取行业成分股,使用模拟数据: {str(e)}")
|
| result = self._generate_mock_industry_stocks(industry)
|
|
|
|
|
| self.data_cache[cache_key] = (datetime.now(), result)
|
|
|
| return result
|
|
|
| except Exception as e:
|
| self.logger.error(f"获取行业成分股失败: {str(e)}")
|
| import traceback
|
| self.logger.error(traceback.format_exc())
|
| return []
|
|
|
| def _generate_mock_industry_stocks(self, industry):
|
| """生成模拟的行业成分股数据"""
|
| self.logger.info(f"生成行业 {industry} 的模拟成分股数据")
|
|
|
|
|
| fund_flow_data = self.get_industry_fund_flow("即时")
|
| industry_data = next((item for item in fund_flow_data if item["industry"] == industry), None)
|
|
|
| company_count = 20
|
| if industry_data and "companyCount" in industry_data:
|
| company_count = min(industry_data["companyCount"], 30)
|
|
|
|
|
| result = []
|
| for i in range(company_count):
|
|
|
| prefix = "6" if i % 2 == 0 else "0"
|
| code = prefix + str(100000 + i).zfill(5)[-5:]
|
|
|
|
|
| price = round(random.uniform(10, 100), 2)
|
| change = round(random.uniform(-5, 5), 2)
|
|
|
|
|
| volume = round(random.uniform(100000, 10000000))
|
| turnover = round(volume * price / 10000, 2)
|
|
|
|
|
| turnover_rate = round(random.uniform(0.5, 5), 2)
|
| amplitude = round(random.uniform(1, 10), 2)
|
|
|
| item = {
|
| "code": code,
|
| "name": f"{industry}股{i + 1}",
|
| "price": price,
|
| "change": change,
|
| "change_amount": round(price * change / 100, 2),
|
| "volume": volume,
|
| "turnover": turnover,
|
| "amplitude": amplitude,
|
| "turnover_rate": turnover_rate
|
| }
|
| result.append(item)
|
|
|
|
|
| result.sort(key=lambda x: x["change"], reverse=True)
|
|
|
| return result
|
|
|
| def get_industry_detail(self, industry):
|
| """获取行业详细信息"""
|
| try:
|
|
|
| fund_flow_data = self.get_industry_fund_flow("即时")
|
| industry_data = next((item for item in fund_flow_data if item["industry"] == industry), None)
|
|
|
| if not industry_data:
|
| return None
|
|
|
|
|
| history_data = []
|
|
|
| for period in ["3日排行", "5日排行", "10日排行", "20日排行"]:
|
| period_data = self.get_industry_fund_flow(period)
|
| industry_period_data = next((item for item in period_data if item["industry"] == industry), None)
|
|
|
| if industry_period_data:
|
| days = int(period.replace("日排行", ""))
|
| date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
|
| history_data.append({
|
| "date": date,
|
| "inflow": industry_period_data["inflow"],
|
| "outflow": industry_period_data["outflow"],
|
| "netFlow": industry_period_data["netFlow"],
|
| "change": industry_period_data["change"]
|
| })
|
|
|
|
|
| history_data.append({
|
| "date": datetime.now().strftime("%Y-%m-%d"),
|
| "inflow": industry_data["inflow"],
|
| "outflow": industry_data["outflow"],
|
| "netFlow": industry_data["netFlow"],
|
| "change": industry_data["change"]
|
| })
|
|
|
|
|
| history_data.sort(key=lambda x: x["date"])
|
|
|
|
|
| score = self.calculate_industry_score(industry_data, history_data)
|
|
|
|
|
| recommendation = self.generate_industry_recommendation(score, industry_data, history_data)
|
|
|
|
|
| result = {
|
| "industry": industry,
|
| "index": industry_data["index"],
|
| "change": industry_data["change"],
|
| "companyCount": industry_data["companyCount"],
|
| "inflow": industry_data["inflow"],
|
| "outflow": industry_data["outflow"],
|
| "netFlow": industry_data["netFlow"],
|
| "leadingStock": industry_data.get("leadingStock", ""),
|
| "leadingStockChange": industry_data.get("leadingStockChange", ""),
|
| "leadingStockPrice": industry_data.get("leadingStockPrice", 0),
|
| "score": score,
|
| "recommendation": recommendation,
|
| "flowHistory": history_data
|
| }
|
|
|
| return result
|
|
|
| except Exception as e:
|
| self.logger.error(f"获取行业详细信息失败: {str(e)}")
|
| import traceback
|
| self.logger.error(traceback.format_exc())
|
| return None
|
|
|
| def calculate_industry_score(self, industry_data, history_data):
|
| """计算行业评分"""
|
| try:
|
|
|
| score = 50
|
|
|
|
|
| change = float(industry_data["change"])
|
| if change > 3:
|
| score += 10
|
| elif change > 1:
|
| score += 5
|
| elif change < -3:
|
| score -= 10
|
| elif change < -1:
|
| score -= 5
|
|
|
|
|
| netFlow = float(industry_data["netFlow"])
|
|
|
| if netFlow > 5:
|
| score += 20
|
| elif netFlow > 2:
|
| score += 15
|
| elif netFlow > 0:
|
| score += 10
|
| elif netFlow < -5:
|
| score -= 20
|
| elif netFlow < -2:
|
| score -= 15
|
| elif netFlow < 0:
|
| score -= 10
|
|
|
|
|
| if len(history_data) >= 2:
|
| net_flow_trend = 0
|
| for i in range(1, len(history_data)):
|
| if float(history_data[i]["netFlow"]) > float(history_data[i - 1]["netFlow"]):
|
| net_flow_trend += 1
|
| else:
|
| net_flow_trend -= 1
|
|
|
| if net_flow_trend > 0:
|
| score += 10
|
| elif net_flow_trend < 0:
|
| score -= 10
|
|
|
|
|
| score = max(0, min(100, score))
|
|
|
| return round(score)
|
|
|
| except Exception as e:
|
| self.logger.error(f"计算行业评分时出错: {str(e)}")
|
| return 50
|
|
|
| def generate_industry_recommendation(self, score, industry_data, history_data):
|
| """生成行业投资建议"""
|
| try:
|
| if score >= 80:
|
| return "行业景气度高,资金持续流入,建议积极配置"
|
| elif score >= 60:
|
| return "行业表现良好,建议适当加仓"
|
| elif score >= 40:
|
| return "行业表现一般,建议谨慎参与"
|
| else:
|
| return "行业下行趋势明显,建议减持规避风险"
|
|
|
| except Exception as e:
|
| self.logger.error(f"生成行业投资建议时出错: {str(e)}")
|
| return "无法生成投资建议"
|
|
|
| def compare_industries(self, limit=10):
|
| """比较不同行业的表现"""
|
| try:
|
|
|
| industry_data = ak.stock_board_industry_name_em()
|
|
|
|
|
| industries = industry_data['板块名称'].tolist() if '板块名称' in industry_data.columns else []
|
|
|
| if not industries:
|
| return {"error": "获取行业列表失败"}
|
|
|
|
|
| industries = industries[:limit] if limit else industries
|
|
|
|
|
| industry_results = []
|
|
|
| for industry in industries:
|
| try:
|
|
|
| industry_code = None
|
| for _, row in industry_data.iterrows():
|
| if row['板块名称'] == industry:
|
| industry_code = row['板块代码']
|
| break
|
|
|
| if not industry_code:
|
| self.logger.warning(f"未找到行业 {industry} 的板块代码")
|
| continue
|
|
|
|
|
| try:
|
|
|
| industry_info = ak.stock_board_industry_hist_em(symbol=industry_code)
|
| except Exception as e1:
|
| try:
|
|
|
| industry_info = ak.stock_board_industry_hist_em(symbol=industry_code, period="daily")
|
| except Exception as e2:
|
| self.logger.warning(f"分析行业 {industry} 历史数据失败: {str(e1)}, {str(e2)}")
|
| continue
|
|
|
|
|
| if not industry_info.empty:
|
| latest = industry_info.iloc[0]
|
|
|
|
|
| change = 0.0
|
| if '涨跌幅' in latest.index:
|
| change = latest['涨跌幅']
|
| elif '涨跌幅' in industry_info.columns:
|
| change = latest['涨跌幅']
|
|
|
|
|
| volume = 0.0
|
| turnover = 0.0
|
| if '成交量' in latest.index:
|
| volume = latest['成交量']
|
| elif '成交量' in industry_info.columns:
|
| volume = latest['成交量']
|
|
|
| if '成交额' in latest.index:
|
| turnover = latest['成交额']
|
| elif '成交额' in industry_info.columns:
|
| turnover = latest['成交额']
|
|
|
| industry_results.append({
|
| "industry": industry,
|
| "change": float(change) if change else 0.0,
|
| "volume": float(volume) if volume else 0.0,
|
| "turnover": float(turnover) if turnover else 0.0
|
| })
|
| except Exception as e:
|
| self.logger.error(f"分析行业 {industry} 时出错: {str(e)}")
|
|
|
|
|
| industry_results.sort(key=lambda x: x.get('change', 0), reverse=True)
|
|
|
| return {
|
| "count": len(industry_results),
|
| "top_industries": industry_results[:5] if len(industry_results) >= 5 else industry_results,
|
| "bottom_industries": industry_results[-5:] if len(industry_results) >= 5 else [],
|
| "results": industry_results
|
| }
|
|
|
| except Exception as e:
|
| self.logger.error(f"比较行业表现时出错: {str(e)}")
|
| return {"error": f"比较行业表现时出错: {str(e)}"} |