|
|
|
"""
|
|
智能分析系统(股票) - 股票市场数据分析系统
|
|
开发者:熊猫大侠
|
|
版本: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)}"} |