import sys
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QFileDialog, QLabel,
QComboBox, QMessageBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap
from io import BytesIO
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class StockApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(" 股票分析(MACD单图+交叉标记)")
self.resize(1000, 700)
self.df = None
# 主界面
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# ========== 控制面板 ==========
ctrl_layout = QHBoxLayout()
self.btn_load = QPushButton(" 加载 Excel")
self.label_file = QLabel("未加载文件")
self.combo_type = QComboBox()
self.combo_type.addItems([" 统计图", "均线图", "MACD图", " 雷达图"])
# 新增:字段选择
self.combo_field = QComboBox()
self.combo_field.addItems(["开盘价", "收盘价", "最高价", "最低价"])
self.combo_field.setToolTip("选择要统计的价格字段")
# 新增:统计方式
self.combo_agg = QComboBox()
self.combo_agg.addItems(["均值", "最大值", "最小值"])
self.combo_agg.setToolTip("选择统计方法")
self.combo_stock = QComboBox()
self.combo_category = QComboBox()
self.btn_plot = QPushButton(" 绘图")
# 信号连接
self.btn_load.clicked.connect(self.load_file)
self.btn_plot.clicked.connect(self.plot_chart)
self.combo_type.currentTextChanged.connect(self.update_controls)
# 添加控件
ctrl_layout.addWidget(self.btn_load)
ctrl_layout.addWidget(self.label_file)
ctrl_layout.addWidget(QLabel("类型:"))
ctrl_layout.addWidget(self.combo_type)
ctrl_layout.addWidget(QLabel("字段:"))
ctrl_layout.addWidget(self.combo_field)
ctrl_layout.addWidget(QLabel("统计:"))
ctrl_layout.addWidget(self.combo_agg)
ctrl_layout.addWidget(QLabel("股票:"))
ctrl_layout.addWidget(self.combo_stock)
ctrl_layout.addWidget(QLabel("类别:"))
ctrl_layout.addWidget(self.combo_category)
ctrl_layout.addWidget(self.btn_plot)
main_layout.addLayout(ctrl_layout)
# ========== 图像显示区域 ==========
self.image_label = QLabel("等待绘图...")
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.image_label.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;")
self.image_label.setMinimumHeight(500)
main_layout.addWidget(self.image_label)
self.update_controls()
def update_controls(self):
t = self.combo_type.currentText()
self.combo_stock.setVisible(t in ["均线图", "MACD图"])
self.combo_category.setVisible(t == " 雷达图")
self.combo_field.setVisible(t == " 统计图")
self.combo_agg.setVisible(t == " 统计图")
def load_file(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)"
)
if not path:
return
try:
xls = pd.ExcelFile(path)
sheets = []
for sheet_name in xls.sheet_names:
df_sheet = pd.read_excel(xls, sheet_name=sheet_name)
if '股票代码' in df_sheet.columns:
cols = ['股票代码', '日期', '开盘价', '收盘价', '最高价', '最低价',
'交易量', '涨跌幅', '振幅', '换手率', '类别']
sheets.append(df_sheet[cols].dropna(how='all'))
self.df = pd.concat(sheets, ignore_index=True).dropna(subset=['股票代码', '日期'])
# 数据清洗
self.df['股票代码'] = pd.to_numeric(self.df['股票代码'], errors='coerce').dropna().astype(int)
self.df['日期'] = pd.to_datetime(self.df['日期'], errors='coerce')
self.df = self.df.dropna(subset=['日期'])
self.df.sort_values(['股票代码', '日期'], inplace=True)
self.label_file.setText(f"✅ 已加载: {path.split('/')[-1]} ({len(self.df)}条)")
# 填充下拉框
stocks = sorted(self.df['股票代码'].unique())
self.combo_stock.clear()
for s in stocks:
self.combo_stock.addItem(str(s))
cats = sorted(self.df['类别'].astype(str).unique())
self.combo_category.clear()
for c in cats:
self.combo_category.addItem(c)
self.update_controls()
except Exception as e:
QMessageBox.critical(self, "❌ 加载失败", f"{str(e)}")
def plot_chart(self):
chart_type = self.combo_type.currentText()
try:
buf = BytesIO()
fig = None
if chart_type == " 统计图":
field = self.combo_field.currentText()
agg_type = self.combo_agg.currentText()
fig = self._create_stats_plot(field, agg_type)
elif chart_type == "均线图":
code_str = self.combo_stock.currentText()
code = int(code_str)
fig = self._create_ma_plot(code)
elif chart_type == "MACD图":
code_str = self.combo_stock.currentText()
code = int(code_str)
fig = self._create_macd_plot(code)
elif chart_type == " 雷达图":
cat = self.combo_category.currentText()
fig = self._create_radar_plot(cat)
if fig is not None:
fig.savefig(buf, format='png', bbox_inches='tight', dpi=100)
plt.close(fig)
buf.seek(0)
pixmap = QPixmap()
pixmap.loadFromData(buf.read(), "PNG")
scaled_pixmap = pixmap.scaled(
self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
self.image_label.setPixmap(scaled_pixmap)
else:
self.image_label.setText("❌ 图表生成失败")
except Exception as e:
plt.close('all')
QMessageBox.critical(self, "绘图失败", f"{type(e).__name__}: \n{str(e)}")
def _create_stats_plot(self, field, agg_type):
"""创建所有股票的统计柱状图"""
group = self.df.groupby('股票代码')[field]
if agg_type == "均值":
data = group.mean().dropna()
op = "均值"
elif agg_type == "最大值":
data = group.max().dropna()
op = "最大值"
elif agg_type == "最小值":
data = group.min().dropna()
op = "最小值"
else:
data = group.mean().dropna()
op = "均值"
width = max(12, len(data) * 0.3)
fig, ax = plt.subplots(figsize=(width, 15), dpi=100)
data.plot(kind='bar', ax=ax, color='steelblue', edgecolor='black', alpha=0.8)
ax.set_title(f"所有股票{field}{op}对比")
ax.set_ylabel("价格")
ax.set_xlabel("股票代码")
ax.grid(True, axis='y', alpha=0.3)
for label in ax.get_xticklabels():
label.set_rotation(90)
return fig
def _create_ma_plot(self, stock_code):
"""创建移动平均线图(含黄金/死亡交叉)"""
data = self.df[self.df['股票代码'] == stock_code].copy()
if data.empty:
raise ValueError(f"找不到股票 {stock_code}")
data['MA5'] = data['收盘价'].rolling(5).mean()
data['MA30'] = data['收盘价'].rolling(30).mean()
# 计算信号和交叉点
data['Signal'] = (data['MA5'] > data['MA30']).astype(int)
data['Cross'] = data['Signal'].diff() # 1 表示黄金交叉,-1 表示死亡交叉
gold_cross = data[data['Cross'] == 1]
death_cross = data[data['Cross'] == -1]
fig, ax = plt.subplots(figsize=(12, 6), dpi=100)
ax.plot(data['日期'], data['收盘价'], label='收盘价', color='black', linewidth=1)
ax.plot(data['日期'], data['MA5'], label='5日均线', color='green', linewidth=1)
ax.plot(data['日期'], data['MA30'], label='30日均线', color='blue', linewidth=1)
if len(gold_cross) > 0:
ax.scatter(gold_cross['日期'], gold_cross['MA5'],
color='gold', marker='^', s=80, label='黄金交叉', zorder=5)
if len(death_cross) > 0:
ax.scatter(death_cross['日期'], death_cross['MA5'],
color='red', marker='v', s=80, label='死亡交叉', zorder=5)
ax.legend()
ax.grid(True)
ax.set_title(f"股票 {stock_code} 移动平均线")
fig.autofmt_xdate()
return fig
def _create_macd_plot(self, stock_code):
data = self.df[self.df['股票代码'] == stock_code].copy()
close = data['收盘价'].replace([np.inf, -np.inf], np.nan).fillna(method='ffill')
ema12 = close.ewm(span=12).mean()
ema26 = close.ewm(span=26).mean()
dif = ema12 - ema26
dea = dif.ewm(span=9).mean()
macd_bar = 2 * (dif - dea)
fig, ax = plt.subplots(figsize=(12, 6), dpi=100)
# 绘制折线
ax.plot(data['日期'], dif, color='orange', label='DIF', linewidth=1.2)
ax.plot(data['日期'], dea, color='cyan', label='DEA', linewidth=1.2)
ax.legend(loc='upper left')
# 绘制柱状图(MACD)
colors = ['red' if val >= 0 else 'green' for val in macd_bar]
ax.bar(data['日期'], macd_bar, color=colors, width=0.8, label='MACD', alpha=0.6)
ax.axhline(0, color='black', linestyle='--', linewidth=0.8)
ax.grid(True, axis='y', alpha=0.3)
ax.set_title(f"股票 {stock_code} MACD 指标(合并图)")
fig.autofmt_xdate()
return fig
def _create_radar_plot(self, category):
group = self.df[self.df['类别'].astype(str).str.strip() == str(category).strip()]
metrics = ['开盘价', '收盘价', '交易量', '换手率']
means = [float(group[m].mean()) if pd.notna(group[m].mean()) else 0.0 for m in metrics]
# 归一化(防止 max == min)
normalized = []
for i, m in enumerate(metrics):
col = self.df[m].dropna()
_min, _max = col.min(), col.max()
if _max == _min:
norm_val = 0.5
else:
norm_val = (means[i] - _min) / (_max - _min)
normalized.append(norm_val)
# 构造角度和标签(原始4个点)
radar_labels = ['开盘价', '收盘价', '交易量', '换手率']
n = len(radar_labels)
angles = [i / n * 2 * np.pi for i in range(n)] # 4 个角度
# 创建极坐标图
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
# 绘制闭合图形(手动添加首尾连接)
ax.plot(angles + [angles[0]], normalized + [normalized[0]],
color='blue', linewidth=2, marker='o')
ax.fill(angles + [angles[0]], normalized + [normalized[0]],
color='blue', alpha=0.25)
print(self.df.groupby('类别')[['交易量', '换手率']].mean())
# ✅ 关键修复:ticks 和 labels 都用原始 4 个
ax.set_xticks(angles)
ax.set_xticklabels(radar_labels)
ax.set_title(f"{category}类股票指标雷达图", size=14, pad=20)
ax.grid(True, alpha=0.3)
return fig
if __name__ == "__main__":
app = QApplication(sys.argv)
window = StockApp()
window.show()
sys.exit(app.exec())