Add files via upload
This commit is contained in:
218
gui.py
Normal file
218
gui.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLineEdit, QPushButton, QTextBrowser,
|
||||
QLabel, QTextEdit, QMessageBox, QProgressBar)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QFont
|
||||
import markdown2
|
||||
from stock_analyzer import StockAnalyzer # 导入股票分析器
|
||||
|
||||
class AnalysisWorker(QThread):
|
||||
"""后台工作线程,用于执行分析任务"""
|
||||
finished = pyqtSignal(dict)
|
||||
error = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
|
||||
def __init__(self, analyzer, stock_code):
|
||||
super().__init__()
|
||||
self.analyzer = analyzer
|
||||
self.stock_code = stock_code
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
report = self.analyzer.analyze_stock(self.stock_code)
|
||||
self.finished.emit(report)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class BatchAnalysisWorker(QThread):
|
||||
"""后台工作线程,用于执行批量分析任务"""
|
||||
finished = pyqtSignal(list)
|
||||
error = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
|
||||
def __init__(self, analyzer, stock_list):
|
||||
super().__init__()
|
||||
self.analyzer = analyzer
|
||||
self.stock_list = stock_list
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
results = []
|
||||
total = len(self.stock_list)
|
||||
for i, stock_code in enumerate(self.stock_list):
|
||||
report = self.analyzer.analyze_stock(stock_code)
|
||||
results.append(report)
|
||||
self.progress.emit(int((i + 1) / total * 100))
|
||||
self.finished.emit(results)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class StockAnalyzerGUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analyzer = StockAnalyzer()
|
||||
self.initUI()
|
||||
|
||||
def initUI(self):
|
||||
self.setWindowTitle('股票分析系统')
|
||||
self.setGeometry(100, 100, 1200, 800)
|
||||
|
||||
# 创建中央部件和布局
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout(central_widget)
|
||||
|
||||
# 创建输入区域
|
||||
input_layout = QHBoxLayout()
|
||||
|
||||
# 单只股票分析部分
|
||||
single_stock_layout = QVBoxLayout()
|
||||
single_label = QLabel('单只股票分析:')
|
||||
single_label.setFont(QFont('Arial', 12, QFont.Weight.Bold))
|
||||
self.single_stock_input = QLineEdit()
|
||||
self.single_stock_input.setPlaceholderText('输入股票代码(如:600000)')
|
||||
self.single_stock_input.setFont(QFont('Arial', 10))
|
||||
self.analyze_btn = QPushButton('分析')
|
||||
self.analyze_btn.setFont(QFont('Arial', 10, QFont.Weight.Bold))
|
||||
self.analyze_btn.clicked.connect(self.analyze_single_stock)
|
||||
single_stock_layout.addWidget(single_label)
|
||||
single_stock_layout.addWidget(self.single_stock_input)
|
||||
single_stock_layout.addWidget(self.analyze_btn)
|
||||
|
||||
# 批量分析部分
|
||||
batch_stock_layout = QVBoxLayout()
|
||||
batch_label = QLabel('批量股票分析:')
|
||||
batch_label.setFont(QFont('Arial', 12, QFont.Weight.Bold))
|
||||
self.batch_stock_input = QTextEdit()
|
||||
self.batch_stock_input.setPlaceholderText('输入多个股票代码,每行一个')
|
||||
self.batch_stock_input.setFont(QFont('Arial', 10))
|
||||
self.batch_stock_input.setMaximumHeight(100)
|
||||
self.batch_analyze_btn = QPushButton('批量分析')
|
||||
self.batch_analyze_btn.setFont(QFont('Arial', 10, QFont.Weight.Bold))
|
||||
self.batch_analyze_btn.clicked.connect(self.analyze_multiple_stocks)
|
||||
batch_stock_layout.addWidget(batch_label)
|
||||
batch_stock_layout.addWidget(self.batch_stock_input)
|
||||
batch_stock_layout.addWidget(self.batch_analyze_btn)
|
||||
|
||||
input_layout.addLayout(single_stock_layout)
|
||||
input_layout.addLayout(batch_stock_layout)
|
||||
|
||||
# 添加进度条
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
# 添加结果显示区域
|
||||
self.result_browser = QTextBrowser()
|
||||
self.result_browser.setOpenExternalLinks(True)
|
||||
self.result_browser.setFont(QFont('Arial', 10))
|
||||
|
||||
layout.addLayout(input_layout)
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addWidget(self.result_browser)
|
||||
|
||||
def format_report(self, report, is_single=True):
|
||||
"""将分析报告格式化为Markdown格式"""
|
||||
md = f"""# 股票分析报告 - {report['stock_code']}
|
||||
|
||||
## 基本信息
|
||||
- 分析日期:{report['analysis_date']}
|
||||
- 当前价格:{report['price']:.2f}
|
||||
- 价格变动:{report['price_change']:.2f}%
|
||||
|
||||
## 技术指标
|
||||
- 均线趋势:{report['ma_trend']}
|
||||
- RSI指标:{report['rsi']:.2f}
|
||||
- MACD信号:{report['macd_signal']}
|
||||
- 成交量状态:{report['volume_status']}
|
||||
|
||||
## 评分与建议
|
||||
- 综合评分:{report['score']}分
|
||||
- 投资建议:{report['recommendation']}
|
||||
|
||||
## AI分析
|
||||
{report['ai_analysis']}
|
||||
|
||||
---
|
||||
"""
|
||||
return md
|
||||
|
||||
def analyze_single_stock(self):
|
||||
"""分析单只股票"""
|
||||
stock_code = self.single_stock_input.text().strip()
|
||||
if not stock_code:
|
||||
QMessageBox.warning(self, '警告', '请输入股票代码')
|
||||
return
|
||||
|
||||
self.analyze_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# 创建工作线程
|
||||
self.worker = AnalysisWorker(self.analyzer, stock_code)
|
||||
self.worker.finished.connect(self.handle_single_analysis_result)
|
||||
self.worker.error.connect(self.handle_analysis_error)
|
||||
self.worker.progress.connect(self.progress_bar.setValue)
|
||||
self.worker.start()
|
||||
|
||||
def handle_single_analysis_result(self, report):
|
||||
"""处理单只股票分析结果"""
|
||||
markdown_text = self.format_report(report)
|
||||
html_content = markdown2.markdown(markdown_text)
|
||||
self.result_browser.setHtml(html_content)
|
||||
self.analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def analyze_multiple_stocks(self):
|
||||
"""批量分析股票"""
|
||||
text = self.batch_stock_input.toPlainText().strip()
|
||||
if not text:
|
||||
QMessageBox.warning(self, '警告', '请输入股票代码')
|
||||
return
|
||||
|
||||
stock_list = [code.strip() for code in text.split('\n') if code.strip()]
|
||||
|
||||
self.batch_analyze_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# 创建工作线程
|
||||
self.batch_worker = BatchAnalysisWorker(self.analyzer, stock_list)
|
||||
self.batch_worker.finished.connect(self.handle_batch_analysis_result)
|
||||
self.batch_worker.error.connect(self.handle_analysis_error)
|
||||
self.batch_worker.progress.connect(self.progress_bar.setValue)
|
||||
self.batch_worker.start()
|
||||
|
||||
def handle_batch_analysis_result(self, recommendations):
|
||||
"""处理批量分析结果"""
|
||||
# 生成markdown格式的报告
|
||||
markdown_text = "# 批量股票分析报告\n\n"
|
||||
for rec in recommendations:
|
||||
markdown_text += self.format_report(rec, False)
|
||||
|
||||
html_content = markdown2.markdown(markdown_text)
|
||||
self.result_browser.setHtml(html_content)
|
||||
self.batch_analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def handle_analysis_error(self, error_message):
|
||||
"""处理分析错误"""
|
||||
QMessageBox.critical(self, '错误', f'分析过程中出现错误:{error_message}')
|
||||
self.analyze_btn.setEnabled(True)
|
||||
self.batch_analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 设置应用程序样式
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# 创建并显示主窗口
|
||||
window = StockAnalyzerGUI()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
497
gui2.py
Normal file
497
gui2.py
Normal file
@@ -0,0 +1,497 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLineEdit, QPushButton, QTextBrowser,
|
||||
QLabel, QTextEdit, QMessageBox, QProgressBar,
|
||||
QFrame, QSizePolicy)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QFont, QPalette, QColor
|
||||
import markdown2
|
||||
from stock_analyzer import StockAnalyzer
|
||||
|
||||
class ModernFrame(QFrame):
|
||||
"""现代化的面板组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised)
|
||||
self.setStyleSheet("""
|
||||
ModernFrame {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
""")
|
||||
|
||||
class ModernButton(QPushButton):
|
||||
"""现代化的按钮组件"""
|
||||
def __init__(self, text, parent=None, primary=True):
|
||||
super().__init__(text, parent)
|
||||
self.setMinimumHeight(40)
|
||||
if primary:
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1557b0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0d47a1;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #f8f9fa;
|
||||
color: #1a73e8;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 5px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #f1f3f4;
|
||||
border-color: #d2e3fc;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #e8eaed;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
color: #5f6368;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
""")
|
||||
|
||||
class ModernLineEdit(QLineEdit):
|
||||
"""现代化的输入框组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setMinimumHeight(40)
|
||||
self.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
padding: 8px 12px;
|
||||
background-color: white;
|
||||
selection-background-color: #cce0ff;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
QLineEdit:hover {
|
||||
border-color: #999999;
|
||||
}
|
||||
""")
|
||||
|
||||
class ModernTextEdit(QTextEdit):
|
||||
"""现代化的多行文本输入框组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setStyleSheet("""
|
||||
QTextEdit {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
background-color: white;
|
||||
selection-background-color: #cce0ff;
|
||||
}
|
||||
QTextEdit:focus {
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
QTextEdit:hover {
|
||||
border-color: #999999;
|
||||
}
|
||||
""")
|
||||
|
||||
class ModernProgressBar(QProgressBar):
|
||||
"""现代化的进度条组件"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setStyleSheet("""
|
||||
QProgressBar {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: #f0f0f0;
|
||||
height: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #1a73e8;
|
||||
border-radius: 3px;
|
||||
}
|
||||
""")
|
||||
self.setTextVisible(False)
|
||||
|
||||
class AnalysisWorker(QThread):
|
||||
"""后台工作线程,用于执行分析任务"""
|
||||
finished = pyqtSignal(dict)
|
||||
error = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
|
||||
def __init__(self, analyzer, stock_code):
|
||||
super().__init__()
|
||||
self.analyzer = analyzer
|
||||
self.stock_code = stock_code
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
report = self.analyzer.analyze_stock(self.stock_code)
|
||||
self.finished.emit(report)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class BatchAnalysisWorker(QThread):
|
||||
"""后台工作线程,用于执行批量分析任务"""
|
||||
finished = pyqtSignal(list)
|
||||
error = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
|
||||
def __init__(self, analyzer, stock_list):
|
||||
super().__init__()
|
||||
self.analyzer = analyzer
|
||||
self.stock_list = stock_list
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
results = []
|
||||
total = len(self.stock_list)
|
||||
for i, stock_code in enumerate(self.stock_list):
|
||||
report = self.analyzer.analyze_stock(stock_code)
|
||||
results.append(report)
|
||||
self.progress.emit(int((i + 1) / total * 100))
|
||||
self.finished.emit(results)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
class ModernStockAnalyzerGUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analyzer = StockAnalyzer()
|
||||
self.init_ui()
|
||||
self.adjust_size_and_position()
|
||||
|
||||
def adjust_size_and_position(self):
|
||||
"""调整窗口大小和位置以适应不同分辨率"""
|
||||
screen = QApplication.primaryScreen()
|
||||
if screen:
|
||||
geometry = screen.availableGeometry()
|
||||
# 设置窗口大小为屏幕的75%
|
||||
width = int(geometry.width() * 0.75)
|
||||
height = int(geometry.height() * 0.75)
|
||||
self.resize(width, height)
|
||||
|
||||
# 居中显示
|
||||
center = geometry.center()
|
||||
frame = self.frameGeometry()
|
||||
frame.moveCenter(center)
|
||||
self.move(frame.topLeft())
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle('现代股票分析系统')
|
||||
self.setStyleSheet("""
|
||||
QMainWindow {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
""")
|
||||
|
||||
# 创建中央部件和主布局
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
main_layout.setSpacing(20)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# 创建标题
|
||||
title_label = QLabel('股票分析系统')
|
||||
title_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #202124;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
""")
|
||||
main_layout.addWidget(title_label, alignment=Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# 创建输入区域容器
|
||||
input_container = ModernFrame()
|
||||
input_layout = QHBoxLayout(input_container)
|
||||
input_layout.setSpacing(20)
|
||||
input_layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# 单只股票分析部分
|
||||
single_stock_frame = self.create_single_stock_section()
|
||||
input_layout.addWidget(single_stock_frame)
|
||||
|
||||
# 分隔线
|
||||
separator = QFrame()
|
||||
separator.setFrameShape(QFrame.Shape.VLine)
|
||||
separator.setStyleSheet("background-color: #e0e0e0;")
|
||||
input_layout.addWidget(separator)
|
||||
|
||||
# 批量分析部分
|
||||
batch_stock_frame = self.create_batch_stock_section()
|
||||
input_layout.addWidget(batch_stock_frame)
|
||||
|
||||
main_layout.addWidget(input_container)
|
||||
|
||||
# 进度条
|
||||
self.progress_bar = ModernProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
main_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 结果显示区域
|
||||
result_frame = ModernFrame()
|
||||
result_layout = QVBoxLayout(result_frame)
|
||||
result_layout.setContentsMargins(15, 15, 15, 15)
|
||||
|
||||
result_label = QLabel('分析结果')
|
||||
result_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #202124;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
result_layout.addWidget(result_label)
|
||||
|
||||
self.result_browser = QTextBrowser()
|
||||
self.result_browser.setOpenExternalLinks(True)
|
||||
self.result_browser.setStyleSheet("""
|
||||
QTextBrowser {
|
||||
border: none;
|
||||
background-color: white;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
""")
|
||||
result_layout.addWidget(self.result_browser)
|
||||
|
||||
main_layout.addWidget(result_frame)
|
||||
|
||||
def create_single_stock_section(self):
|
||||
"""创建单只股票分析部分"""
|
||||
frame = QFrame()
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setSpacing(15)
|
||||
|
||||
label = QLabel('单只股票分析')
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #202124;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(label)
|
||||
|
||||
self.single_stock_input = ModernLineEdit()
|
||||
self.single_stock_input.setPlaceholderText('输入股票代码(如:600000)')
|
||||
layout.addWidget(self.single_stock_input)
|
||||
|
||||
self.analyze_btn = ModernButton('分析')
|
||||
self.analyze_btn.clicked.connect(self.analyze_single_stock)
|
||||
layout.addWidget(self.analyze_btn)
|
||||
|
||||
layout.addStretch()
|
||||
return frame
|
||||
|
||||
def create_batch_stock_section(self):
|
||||
"""创建批量分析部分"""
|
||||
frame = QFrame()
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setSpacing(15)
|
||||
|
||||
label = QLabel('批量股票分析')
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #202124;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(label)
|
||||
|
||||
self.batch_stock_input = ModernTextEdit()
|
||||
self.batch_stock_input.setPlaceholderText('输入多个股票代码,每行一个')
|
||||
self.batch_stock_input.setMinimumHeight(100)
|
||||
layout.addWidget(self.batch_stock_input)
|
||||
|
||||
self.batch_analyze_btn = ModernButton('批量分析')
|
||||
self.batch_analyze_btn.clicked.connect(self.analyze_multiple_stocks)
|
||||
layout.addWidget(self.batch_analyze_btn)
|
||||
|
||||
layout.addStretch()
|
||||
return frame
|
||||
|
||||
def format_report(self, report, is_single=True):
|
||||
"""将分析报告格式化为现代化的Markdown格式"""
|
||||
md = f"""# 股票分析报告 - {report['stock_code']}
|
||||
|
||||
## 基本信息
|
||||
- **分析日期:** {report['analysis_date']}
|
||||
- **当前价格:** ¥{report['price']:.2f}
|
||||
- **价格变动:** {report['price_change']:.2f}%
|
||||
|
||||
## 技术指标
|
||||
- **均线趋势:** {report['ma_trend']}
|
||||
- **RSI指标:** {report['rsi']:.2f}
|
||||
- **MACD信号:** {report['macd_signal']}
|
||||
- **成交量状态:** {report['volume_status']}
|
||||
|
||||
## 评分与建议
|
||||
- **综合评分:** {report['score']}分
|
||||
- **投资建议:** {report['recommendation']}
|
||||
|
||||
## AI分析
|
||||
{report['ai_analysis']}
|
||||
|
||||
---
|
||||
"""
|
||||
return md
|
||||
|
||||
def analyze_single_stock(self):
|
||||
"""分析单只股票"""
|
||||
stock_code = self.single_stock_input.text().strip()
|
||||
if not stock_code:
|
||||
self.show_warning('请输入股票代码')
|
||||
return
|
||||
|
||||
self.analyze_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.worker = AnalysisWorker(self.analyzer, stock_code)
|
||||
self.worker.finished.connect(self.handle_single_analysis_result)
|
||||
self.worker.error.connect(self.handle_analysis_error)
|
||||
self.worker.progress.connect(self.progress_bar.setValue)
|
||||
self.worker.start()
|
||||
|
||||
def analyze_multiple_stocks(self):
|
||||
"""批量分析股票"""
|
||||
text = self.batch_stock_input.toPlainText().strip()
|
||||
if not text:
|
||||
self.show_warning('请输入股票代码')
|
||||
return
|
||||
|
||||
stock_list = [code.strip() for code in text.split('\n') if code.strip()]
|
||||
|
||||
self.batch_analyze_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
self.batch_worker = BatchAnalysisWorker(self.analyzer, stock_list)
|
||||
self.batch_worker.finished.connect(self.handle_batch_analysis_result)
|
||||
self.batch_worker.error.connect(self.handle_analysis_error)
|
||||
self.batch_worker.progress.connect(self.progress_bar.setValue)
|
||||
self.batch_worker.start()
|
||||
|
||||
def handle_single_analysis_result(self, report):
|
||||
"""处理单只股票分析结果"""
|
||||
markdown_text = self.format_report(report)
|
||||
html_content = markdown2.markdown(markdown_text, extras=['tables', 'fenced-code-blocks'])
|
||||
self.result_browser.setHtml(html_content)
|
||||
self.analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def handle_batch_analysis_result(self, recommendations):
|
||||
"""处理批量分析结果"""
|
||||
markdown_text = "# 批量股票分析报告\n\n"
|
||||
for rec in recommendations:
|
||||
markdown_text += self.format_report(rec, False)
|
||||
|
||||
html_content = markdown2.markdown(markdown_text, extras=['tables', 'fenced-code-blocks'])
|
||||
self.result_browser.setHtml(html_content)
|
||||
self.batch_analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def handle_analysis_error(self, error_message):
|
||||
"""处理分析错误"""
|
||||
self.show_error(f'分析过程中出现错误:{error_message}')
|
||||
self.analyze_btn.setEnabled(True)
|
||||
self.batch_analyze_btn.setEnabled(True)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
def show_warning(self, message):
|
||||
"""显示警告对话框"""
|
||||
warning = QMessageBox(self)
|
||||
warning.setIcon(QMessageBox.Icon.Warning)
|
||||
warning.setWindowTitle('警告')
|
||||
warning.setText(message)
|
||||
warning.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
warning.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #202124;
|
||||
min-width: 200px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
min-width: 80px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1557b0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0d47a1;
|
||||
}
|
||||
""")
|
||||
warning.exec()
|
||||
|
||||
def show_error(self, message):
|
||||
"""显示错误对话框"""
|
||||
error = QMessageBox(self)
|
||||
error.setIcon(QMessageBox.Icon.Critical)
|
||||
error.setWindowTitle('错误')
|
||||
error.setText(message)
|
||||
error.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
error.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #202124;
|
||||
min-width: 200px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #1a73e8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
min-width: 80px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1557b0;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0d47a1;
|
||||
}
|
||||
""")
|
||||
error.exec()
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# 创建并显示主窗口
|
||||
window = ModernStockAnalyzerGUI()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
313
stock_analyzer.py
Normal file
313
stock_analyzer.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
class StockAnalyzer:
|
||||
def __init__(self, initial_cash=1000000):
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 设置 Gemini API
|
||||
self.gemini_api_url = "https://api.linzefeng.top"
|
||||
self.gemini_api_key = os.getenv('GEMINI_API_KEY')
|
||||
|
||||
# 配置参数
|
||||
self.params = {
|
||||
'ma_periods': {'short': 5, 'medium': 20, 'long': 60},
|
||||
'rsi_period': 14,
|
||||
'bollinger_period': 20,
|
||||
'bollinger_std': 2,
|
||||
'volume_ma_period': 20,
|
||||
'atr_period': 14
|
||||
}
|
||||
|
||||
def get_stock_data(self, stock_code, start_date=None, end_date=None):
|
||||
"""获取股票数据"""
|
||||
import akshare as ak
|
||||
|
||||
if start_date is None:
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
if end_date is None:
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
try:
|
||||
# 使用 akshare 获取股票数据
|
||||
df = ak.stock_zh_a_hist(symbol=stock_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
adjust="qfq")
|
||||
|
||||
# 重命名列名以匹配分析需求
|
||||
df = df.rename(columns={
|
||||
"日期": "date",
|
||||
"开盘": "open",
|
||||
"收盘": "close",
|
||||
"最高": "high",
|
||||
"最低": "low",
|
||||
"成交量": "volume"
|
||||
})
|
||||
|
||||
# 确保日期格式正确
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
|
||||
# 数据类型转换
|
||||
numeric_columns = ['open', 'close', 'high', 'low', 'volume']
|
||||
df[numeric_columns] = df[numeric_columns].apply(pd.to_numeric, errors='coerce')
|
||||
|
||||
# 删除空值
|
||||
df = df.dropna()
|
||||
|
||||
return df.sort_values('date')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取股票数据失败: {str(e)}")
|
||||
raise Exception(f"获取股票数据失败: {str(e)}")
|
||||
|
||||
def calculate_ema(self, series, period):
|
||||
"""计算指数移动平均线"""
|
||||
return series.ewm(span=period, adjust=False).mean()
|
||||
|
||||
def calculate_rsi(self, series, period):
|
||||
"""计算RSI指标"""
|
||||
delta = series.diff()
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
||||
rs = gain / loss
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
def calculate_macd(self, series):
|
||||
"""计算MACD指标"""
|
||||
exp1 = series.ewm(span=12, adjust=False).mean()
|
||||
exp2 = series.ewm(span=26, adjust=False).mean()
|
||||
macd = exp1 - exp2
|
||||
signal = macd.ewm(span=9, adjust=False).mean()
|
||||
hist = macd - signal
|
||||
return macd, signal, hist
|
||||
|
||||
def calculate_bollinger_bands(self, series, period, std_dev):
|
||||
"""计算布林带"""
|
||||
middle = series.rolling(window=period).mean()
|
||||
std = series.rolling(window=period).std()
|
||||
upper = middle + (std * std_dev)
|
||||
lower = middle - (std * std_dev)
|
||||
return upper, middle, lower
|
||||
|
||||
def calculate_atr(self, df, period):
|
||||
"""计算ATR指标"""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close'].shift(1)
|
||||
|
||||
tr1 = high - low
|
||||
tr2 = abs(high - close)
|
||||
tr3 = abs(low - close)
|
||||
|
||||
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||
return tr.rolling(window=period).mean()
|
||||
|
||||
def calculate_indicators(self, df):
|
||||
"""计算技术指标"""
|
||||
try:
|
||||
# 计算移动平均线
|
||||
df['MA5'] = self.calculate_ema(df['close'], self.params['ma_periods']['short'])
|
||||
df['MA20'] = self.calculate_ema(df['close'], self.params['ma_periods']['medium'])
|
||||
df['MA60'] = self.calculate_ema(df['close'], self.params['ma_periods']['long'])
|
||||
|
||||
# 计算RSI
|
||||
df['RSI'] = self.calculate_rsi(df['close'], self.params['rsi_period'])
|
||||
|
||||
# 计算MACD
|
||||
df['MACD'], df['Signal'], df['MACD_hist'] = self.calculate_macd(df['close'])
|
||||
|
||||
# 计算布林带
|
||||
df['BB_upper'], df['BB_middle'], df['BB_lower'] = self.calculate_bollinger_bands(
|
||||
df['close'],
|
||||
self.params['bollinger_period'],
|
||||
self.params['bollinger_std']
|
||||
)
|
||||
|
||||
# 成交量分析
|
||||
df['Volume_MA'] = df['volume'].rolling(window=self.params['volume_ma_period']).mean()
|
||||
df['Volume_Ratio'] = df['volume'] / df['Volume_MA']
|
||||
|
||||
# 计算ATR和波动率
|
||||
df['ATR'] = self.calculate_atr(df, self.params['atr_period'])
|
||||
df['Volatility'] = df['ATR'] / df['close'] * 100
|
||||
|
||||
# 动量指标
|
||||
df['ROC'] = df['close'].pct_change(periods=10) * 100
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"计算技术指标时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def calculate_score(self, df):
|
||||
"""计算股票评分"""
|
||||
try:
|
||||
score = 0
|
||||
latest = df.iloc[-1]
|
||||
|
||||
# 趋势得分 (30分)
|
||||
if latest['MA5'] > latest['MA20']:
|
||||
score += 15
|
||||
if latest['MA20'] > latest['MA60']:
|
||||
score += 15
|
||||
|
||||
# RSI得分 (20分)
|
||||
if 30 <= latest['RSI'] <= 70:
|
||||
score += 20
|
||||
elif latest['RSI'] < 30: # 超卖
|
||||
score += 15
|
||||
|
||||
# MACD得分 (20分)
|
||||
if latest['MACD'] > latest['Signal']:
|
||||
score += 20
|
||||
|
||||
# 成交量得分 (30分)
|
||||
if latest['Volume_Ratio'] > 1.5:
|
||||
score += 30
|
||||
elif latest['Volume_Ratio'] > 1:
|
||||
score += 15
|
||||
|
||||
return score
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"计算评分时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_ai_analysis(self, df, stock_code):
|
||||
"""使用 Gemini 进行 AI 分析"""
|
||||
try:
|
||||
recent_data = df.tail(14).to_dict('records')
|
||||
|
||||
technical_summary = {
|
||||
'trend': 'upward' if df.iloc[-1]['MA5'] > df.iloc[-1]['MA20'] else 'downward',
|
||||
'volatility': f"{df.iloc[-1]['Volatility']:.2f}%",
|
||||
'volume_trend': 'increasing' if df.iloc[-1]['Volume_Ratio'] > 1 else 'decreasing',
|
||||
'rsi_level': df.iloc[-1]['RSI']
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
分析股票 {stock_code}:
|
||||
|
||||
技术指标概要:
|
||||
{technical_summary}
|
||||
|
||||
近14日交易数据:
|
||||
{recent_data}
|
||||
|
||||
请提供:
|
||||
1. 趋势分析(包含支撑位和压力位)
|
||||
2. 成交量分析及其含义
|
||||
3. 风险评估(包含波动率分析)
|
||||
4. 短期和中期目标价位
|
||||
5. 关键技术位分析
|
||||
6. 具体交易建议(包含止损位)
|
||||
|
||||
请基于技术指标和市场动态进行分析,给出具体数据支持。
|
||||
"""
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.gemini_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": "gemini-1.5-flash",
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.gemini_api_url}/v1/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()['choices'][0]['message']['content']
|
||||
else:
|
||||
return "AI 分析暂时无法使用"
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"AI 分析发生错误: {str(e)}")
|
||||
return "AI 分析过程中发生错误"
|
||||
|
||||
def get_recommendation(self, score):
|
||||
"""根据得分给出建议"""
|
||||
if score >= 80:
|
||||
return '强烈推荐买入'
|
||||
elif score >= 60:
|
||||
return '建议买入'
|
||||
elif score >= 40:
|
||||
return '观望'
|
||||
elif score >= 20:
|
||||
return '建议卖出'
|
||||
else:
|
||||
return '强烈建议卖出'
|
||||
|
||||
def analyze_stock(self, stock_code):
|
||||
"""分析单个股票"""
|
||||
try:
|
||||
# 获取股票数据
|
||||
df = self.get_stock_data(stock_code)
|
||||
|
||||
# 计算技术指标
|
||||
df = self.calculate_indicators(df)
|
||||
|
||||
# 评分系统
|
||||
score = self.calculate_score(df)
|
||||
|
||||
# 获取最新数据
|
||||
latest = df.iloc[-1]
|
||||
prev = df.iloc[-2]
|
||||
|
||||
# 生成报告(保持原有格式)
|
||||
report = {
|
||||
'stock_code': stock_code,
|
||||
'analysis_date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'score': score,
|
||||
'price': latest['close'],
|
||||
'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
|
||||
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
|
||||
'rsi': latest['RSI'],
|
||||
'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
|
||||
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
|
||||
'recommendation': self.get_recommendation(score),
|
||||
'ai_analysis': self.get_ai_analysis(df, stock_code)
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"分析股票时出错: {str(e)}")
|
||||
raise
|
||||
|
||||
def scan_market(self, stock_list, min_score=60):
|
||||
"""扫描市场,寻找符合条件的股票"""
|
||||
recommendations = []
|
||||
|
||||
for stock_code in stock_list:
|
||||
try:
|
||||
report = self.analyze_stock(stock_code)
|
||||
if report['score'] >= min_score:
|
||||
recommendations.append(report)
|
||||
except Exception as e:
|
||||
self.logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
# 按得分排序
|
||||
recommendations.sort(key=lambda x: x['score'], reverse=True)
|
||||
return recommendations
|
||||
602
全部股票分析推荐1.py
Normal file
602
全部股票分析推荐1.py
Normal file
@@ -0,0 +1,602 @@
|
||||
""""
|
||||
Stock Analysis System
|
||||
优化后的全盘股票技术分析系统——用于A股市场股票的全面分析,已加速分析并增加额外指标。
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import akshare as ak
|
||||
from tqdm import tqdm
|
||||
|
||||
# -------------------------------
|
||||
# **技术指标配置**
|
||||
# -------------------------------
|
||||
@dataclass
|
||||
class TechnicalParams:
|
||||
"""技术指标参数配置"""
|
||||
ma_periods: Dict[str, int]
|
||||
rsi_period: int
|
||||
bollinger_period: int
|
||||
bollinger_std: int
|
||||
volume_ma_period: int
|
||||
atr_period: int
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> 'TechnicalParams':
|
||||
"""返回默认的技术指标参数"""
|
||||
return cls(
|
||||
ma_periods={'short': 5, 'medium': 20, 'long': 60},
|
||||
rsi_period=14,
|
||||
bollinger_period=20,
|
||||
bollinger_std=2,
|
||||
volume_ma_period=20,
|
||||
atr_period=14
|
||||
)
|
||||
|
||||
# -------------------------------
|
||||
# **股票分析引擎**
|
||||
# -------------------------------
|
||||
class StockAnalyzer:
|
||||
"""股票分析引擎,计算各类技术指标"""
|
||||
|
||||
def __init__(self, params: Optional[TechnicalParams] = None):
|
||||
"""
|
||||
初始化股票分析引擎
|
||||
|
||||
Args:
|
||||
params: 技术指标配置参数
|
||||
"""
|
||||
self._setup_logging()
|
||||
self.params = params or TechnicalParams.default()
|
||||
|
||||
def _setup_logging(self) -> None:
|
||||
"""配置日志记录"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_stock_data(self, stock_code: str,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取单只股票历史数据,默认使用前一年的数据。
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码(可以带市场前缀或纯代码)
|
||||
start_date: 开始日期(格式YYYYMMDD)
|
||||
end_date: 结束日期(格式YYYYMMDD)
|
||||
|
||||
Returns:
|
||||
包含日期、开盘、收盘、最高、最低、成交量的 DataFrame
|
||||
"""
|
||||
try:
|
||||
if not start_date:
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
if not end_date:
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
code = stock_code[2:] if stock_code.startswith(('sz', 'sh')) else stock_code
|
||||
|
||||
df = ak.stock_zh_a_hist(
|
||||
symbol=code,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
adjust="qfq"
|
||||
)
|
||||
|
||||
self.logger.info(f"获取到 {len(df)} 行数据,列名:{df.columns.tolist()}")
|
||||
|
||||
df = df.rename(columns={
|
||||
"日期": "date",
|
||||
"开盘": "open",
|
||||
"收盘": "close",
|
||||
"最高": "high",
|
||||
"最低": "low",
|
||||
"成交量": "volume",
|
||||
"trade_date": "date"
|
||||
})
|
||||
|
||||
required_columns = {'date', 'open', 'close', 'high', 'low', 'volume'}
|
||||
missing_columns = required_columns - set(df.columns)
|
||||
if missing_columns:
|
||||
raise ValueError(f"缺失必须字段: {missing_columns}")
|
||||
|
||||
df['date'] = pd.to_datetime(df['date'], errors='coerce')
|
||||
numeric_columns = ['open', 'close', 'high', 'low', 'volume']
|
||||
df[numeric_columns] = df[numeric_columns].apply(pd.to_numeric, errors='coerce')
|
||||
df_cleaned = df.dropna(subset=['date'] + numeric_columns).sort_values('date')
|
||||
|
||||
if len(df_cleaned) < 60:
|
||||
raise ValueError(f"数据不足(仅 {len(df_cleaned)} 行),无法计算至少60日均线")
|
||||
|
||||
return df_cleaned
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取股票数据失败,股票代码 {stock_code},错误信息:{str(e)}")
|
||||
raise ValueError(f"股票 {stock_code} 数据获取出错: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def calculate_ema(series: pd.Series, period: int) -> pd.Series:
|
||||
"""计算 EMA"""
|
||||
return series.ewm(span=period, adjust=False).mean()
|
||||
|
||||
@staticmethod
|
||||
def calculate_rsi(series: pd.Series, period: int) -> pd.Series:
|
||||
"""
|
||||
计算 RSI(指数加权移动平均法)
|
||||
"""
|
||||
delta = series.diff()
|
||||
gain = delta.clip(lower=0)
|
||||
loss = -delta.clip(upper=0)
|
||||
avg_gain = gain.ewm(com=period - 1, adjust=False).mean()
|
||||
avg_loss = loss.ewm(com=period - 1, adjust=False).mean()
|
||||
rs = avg_gain / (avg_loss + 1e-10)
|
||||
return 100 - (100 / (1 + rs))
|
||||
|
||||
@staticmethod
|
||||
def calculate_macd(series: pd.Series) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""计算 MACD、信号线和直方图"""
|
||||
exp1 = series.ewm(span=12, adjust=False).mean()
|
||||
exp2 = series.ewm(span=26, adjust=False).mean()
|
||||
macd = exp1 - exp2
|
||||
signal = macd.ewm(span=9, adjust=False).mean()
|
||||
return macd, signal, macd - signal
|
||||
|
||||
@staticmethod
|
||||
def calculate_bollinger_bands(series: pd.Series, period: int, std_dev: int) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""计算 Bollinger 通道"""
|
||||
middle = series.rolling(window=period, min_periods=period).mean()
|
||||
std = series.rolling(window=period, min_periods=period).std()
|
||||
upper = middle + std * std_dev
|
||||
lower = middle - std * std_dev
|
||||
return upper, middle, lower
|
||||
|
||||
def calculate_atr(self, df: pd.DataFrame, period: int) -> pd.Series:
|
||||
"""计算 ATR"""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
prev_close = df['close'].shift(1)
|
||||
tr = pd.concat([
|
||||
high - low,
|
||||
(high - prev_close).abs(),
|
||||
(low - prev_close).abs()
|
||||
], axis=1).max(axis=1)
|
||||
return tr.rolling(window=period, min_periods=period).mean()
|
||||
|
||||
@staticmethod
|
||||
def calculate_obv(series_close: pd.Series, series_volume: pd.Series) -> pd.Series:
|
||||
"""计算 OBV(能量潮指标)"""
|
||||
diff = series_close.diff().fillna(0)
|
||||
obv = np.where(diff > 0, series_volume, np.where(diff < 0, -series_volume, 0))
|
||||
return pd.Series(obv, index=series_close.index).cumsum()
|
||||
|
||||
@staticmethod
|
||||
def calculate_stochastic(series_close: pd.Series, window: int = 14) -> Tuple[pd.Series, pd.Series]:
|
||||
"""
|
||||
计算随机指标(Stochastic Oscillator)
|
||||
%K = (close - lowest_low) / (highest_high - lowest_low)*100
|
||||
%D 为 %K 的3日简单移动平均
|
||||
"""
|
||||
lowest = series_close.rolling(window=window, min_periods=window).min()
|
||||
highest = series_close.rolling(window=window, min_periods=window).max()
|
||||
percentK = (series_close - lowest) / (highest - lowest + 1e-10) * 100
|
||||
percentD = percentK.rolling(window=3, min_periods=3).mean()
|
||||
return percentK, percentD
|
||||
|
||||
def calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""计算所有技术指标,并增加 OBV 和随机指标"""
|
||||
try:
|
||||
# 移动均线
|
||||
for key, period in self.params.ma_periods.items():
|
||||
df[f'MA{period}'] = self.calculate_ema(df['close'], period)
|
||||
df['RSI'] = self.calculate_rsi(df['close'], self.params.rsi_period)
|
||||
df['MACD'], df['Signal'], df['MACD_hist'] = self.calculate_macd(df['close'])
|
||||
df['BB_upper'], df['BB_middle'], df['BB_lower'] = self.calculate_bollinger_bands(
|
||||
df['close'], self.params.bollinger_period, self.params.bollinger_std)
|
||||
df['Volume_MA'] = df['volume'].rolling(window=self.params.volume_ma_period,
|
||||
min_periods=self.params.volume_ma_period).mean()
|
||||
df['Volume_Ratio'] = df['volume'] / (df['Volume_MA'] + 1e-10)
|
||||
df['ATR'] = self.calculate_atr(df, self.params.atr_period)
|
||||
df['Volatility'] = df['ATR'] / df['close'] * 100
|
||||
df['ROC'] = df['close'].pct_change(periods=10) * 100
|
||||
|
||||
# 增加 OBV 指标
|
||||
df['OBV'] = self.calculate_obv(df['close'], df['volume'])
|
||||
df['OBV_MA10'] = df['OBV'].rolling(window=10, min_periods=10).mean()
|
||||
|
||||
# 增加随机指标 Stochastic
|
||||
df['%K'], df['%D'] = self.calculate_stochastic(df['close'], window=14)
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"指标计算出错:{str(e)}")
|
||||
raise
|
||||
|
||||
def calculate_score(self, df: pd.DataFrame) -> float:
|
||||
"""
|
||||
计算股票综合打分(基本打分满分100分),并根据 OBV 与随机指标调整±5分,
|
||||
使量化结果更全面。
|
||||
基本打分逻辑不变:
|
||||
趋势(30分)、RSI(20分)、MACD(20分)、成交量(30分)
|
||||
附加指标调整:
|
||||
OBV:OBV > OBV_MA10,+5分;反之,-5分;
|
||||
随机指标:%K < 20为超卖,+5分;%K > 80为超买,-5分。
|
||||
"""
|
||||
try:
|
||||
score = 0
|
||||
latest = df.iloc[-1]
|
||||
# 趋势打分
|
||||
if latest['MA5'] > latest['MA20'] and latest['MA20'] > latest['MA60']:
|
||||
score += 30
|
||||
else:
|
||||
if latest['MA5'] > latest['MA20']:
|
||||
score += 15
|
||||
if latest['MA20'] > latest['MA60']:
|
||||
score += 15
|
||||
# RSI 打分
|
||||
if 30 <= latest['RSI'] <= 70:
|
||||
score += 20
|
||||
elif latest['RSI'] < 30:
|
||||
score += 15
|
||||
# MACD 打分
|
||||
if latest['MACD'] > latest['Signal']:
|
||||
score += 20
|
||||
# 成交量打分
|
||||
if latest['Volume_Ratio'] > 1.5:
|
||||
score += 30
|
||||
elif latest['Volume_Ratio'] > 1:
|
||||
score += 15
|
||||
|
||||
# 附加 OBV 调整
|
||||
if latest['OBV'] > latest['OBV_MA10']:
|
||||
score += 5
|
||||
else:
|
||||
score -= 5
|
||||
|
||||
# 附加随机指标调整
|
||||
if latest['%K'] < 20:
|
||||
score += 5
|
||||
elif latest['%K'] > 80:
|
||||
score -= 5
|
||||
|
||||
return score
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"计算打分失败:{str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_recommendation(score: float) -> str:
|
||||
"""根据最终打分给出投资建议"""
|
||||
if score >= 80:
|
||||
return '强烈推荐买入'
|
||||
elif score >= 60:
|
||||
return '建议买入'
|
||||
elif score >= 40:
|
||||
return '建议观望'
|
||||
elif score >= 20:
|
||||
return '建议卖出'
|
||||
else:
|
||||
return '强烈建议卖出'
|
||||
|
||||
def analyze_stock(self, stock_code: str) -> Dict:
|
||||
"""针对单只股票执行完整的技术分析流程"""
|
||||
try:
|
||||
df = self.get_stock_data(stock_code)
|
||||
df = self.calculate_indicators(df)
|
||||
score = self.calculate_score(df)
|
||||
latest = df.iloc[-1]
|
||||
prev = df.iloc[-2]
|
||||
return {
|
||||
'stock_code': stock_code,
|
||||
'analysis_date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'score': score,
|
||||
'price': latest['close'],
|
||||
'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
|
||||
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
|
||||
'rsi': latest['RSI'],
|
||||
'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
|
||||
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
|
||||
'recommendation': self.get_recommendation(score)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"分析股票 {stock_code} 失败:{str(e)}")
|
||||
raise
|
||||
|
||||
# -------------------------------
|
||||
# **全盘股票扫描器**
|
||||
# -------------------------------
|
||||
class TopStockScanner:
|
||||
"""全盘筛选高打分股票的扫描器"""
|
||||
|
||||
def __init__(self, max_workers: int = 20, min_score: float = 85):
|
||||
"""
|
||||
初始化扫描器
|
||||
|
||||
Args:
|
||||
max_workers: 并发线程数量(已增至20以加速分析)
|
||||
min_score: 高分最低阈值
|
||||
"""
|
||||
self.analyzer = StockAnalyzer()
|
||||
self.max_workers = max_workers
|
||||
self.min_score = min_score
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_all_stocks(self) -> List[str]:
|
||||
"""
|
||||
获取所有上市 A 股股票代码(全盘版)。
|
||||
使用 ak.stock_info_sh_name_code(symbol="主板A股") 与 ak.stock_info_sz_name_code(symbol="A股列表"),
|
||||
候选字段列表为:['A股代码', '证券代码', '股票代码', 'code'] 。
|
||||
"""
|
||||
try:
|
||||
sh_df = ak.stock_info_sh_name_code(symbol="主板A股")
|
||||
sz_df = ak.stock_info_sz_name_code(symbol="A股列表")
|
||||
candidate_cols = ['A股代码', '证券代码', '股票代码', 'code']
|
||||
|
||||
def get_codes(df: pd.DataFrame) -> set:
|
||||
for col in candidate_cols:
|
||||
if col in df.columns:
|
||||
return {str(code).zfill(6) for code in df[col]}
|
||||
raise KeyError(f"未能找到股票代码字段,现有字段:{df.columns.tolist()}")
|
||||
|
||||
sh_codes = get_codes(sh_df)
|
||||
sz_codes = get_codes(sz_df)
|
||||
all_codes = sorted(sh_codes | sz_codes)
|
||||
self.logger.info(f"完整股票列表获取到 {len(all_codes)} 支股票信息")
|
||||
print(f"\n开始分析 {len(all_codes)} 支股票...")
|
||||
return all_codes
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取股票列表失败:{str(e)}")
|
||||
raise
|
||||
|
||||
def analyze_stock_safe(self, stock_code: str, max_retries: int = 3) -> Optional[Dict]:
|
||||
"""
|
||||
安全分析单只股票(加入重试机制),数据异常则跳过。
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return self.analyzer.analyze_stock(stock_code)
|
||||
except ValueError as e:
|
||||
self.logger.warning(f"跳过股票 {stock_code}: {str(e)}")
|
||||
return None
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
self.logger.error(f"股票 {stock_code} 分析尝试 {max_retries} 次后失败:{str(e)}")
|
||||
return None
|
||||
self.logger.warning(f"股票 {stock_code} 第 {attempt+1} 次分析失败:{str(e)}")
|
||||
time.sleep(random.uniform(2, 5))
|
||||
|
||||
def process_batch(self, stock_codes: List[str]) -> List[Dict]:
|
||||
"""利用多线程并行处理一批股票的分析任务"""
|
||||
results = []
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
futures = {executor.submit(self.analyze_stock_safe, code): code for code in stock_codes}
|
||||
for future in tqdm(futures, desc="分析进度", ncols=80):
|
||||
try:
|
||||
result = future.result()
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
stock = futures[future]
|
||||
self.logger.error(f"处理股票 {stock} 时出错:{str(e)}")
|
||||
return results
|
||||
|
||||
def save_intermediate_results(self, results: List[Dict]) -> None:
|
||||
"""周期性保存中间结果,便于后续查看进度"""
|
||||
try:
|
||||
df = pd.DataFrame(results)
|
||||
high_score_stocks = df[df['score'] >= self.min_score].sort_values('score', ascending=False)
|
||||
output_lines = [
|
||||
"=" * 80,
|
||||
f"股票扫描中间结果 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"共分析 {len(results)} 支股票",
|
||||
"=" * 80,
|
||||
f"\n发现 {len(high_score_stocks)} 支高分股票(得分≥{self.min_score}):"
|
||||
]
|
||||
for _, row in high_score_stocks.iterrows():
|
||||
output_lines.extend([
|
||||
f"\n股票代码: {row['stock_code']}",
|
||||
f"得分: {row['score']:.1f} | 价格: ¥{row['price']:.2f} | 涨跌幅: {row['price_change']:.2f}%"
|
||||
])
|
||||
|
||||
os.makedirs('scanner', exist_ok=True)
|
||||
with open('scanner/temp_results.txt', 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(output_lines))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"保存中间结果失败:{str(e)}")
|
||||
|
||||
def get_high_score_stocks(self, batch_size: int = 20) -> List[Dict]:
|
||||
"""扫描全盘股票,返回高打分结果列表"""
|
||||
try:
|
||||
all_stocks = self.get_all_stocks()
|
||||
total_stocks = len(all_stocks)
|
||||
print(f"\n开始扫描 {total_stocks} 支股票……")
|
||||
results = []
|
||||
total_batches = (total_stocks + batch_size - 1) // batch_size
|
||||
|
||||
for i in range(0, total_stocks, batch_size):
|
||||
batch_number = i // batch_size + 1
|
||||
print(f"\r当前进度: 批次 {batch_number}/{total_batches}", end="")
|
||||
batch = all_stocks[i:i + batch_size]
|
||||
batch_results = self.process_batch(batch)
|
||||
results.extend(batch_results)
|
||||
if i + batch_size < total_stocks:
|
||||
time.sleep(random.uniform(3, 5))
|
||||
if results and ((len(results) % 100 == 0) or (i + batch_size >= total_stocks)):
|
||||
self.save_intermediate_results(results)
|
||||
print("\n扫描结束!")
|
||||
|
||||
if results:
|
||||
df_results = pd.DataFrame(results)
|
||||
high_score_stocks = df_results[df_results['score'] >= self.min_score].sort_values('score', ascending=False)
|
||||
formatted_results = []
|
||||
for _, row in high_score_stocks.iterrows():
|
||||
formatted_results.append({
|
||||
'股票代码': row['stock_code'],
|
||||
'评分': f"{row['score']:.1f}",
|
||||
'当前价格': f"¥{row['price']:.2f}",
|
||||
'涨跌幅': f"{row['price_change']:.2f}%",
|
||||
'RSI指标': f"{row['rsi']:.2f}",
|
||||
'均线趋势': '上升' if row['ma_trend'] == 'UP' else '下降',
|
||||
'MACD信号': '买入' if row['macd_signal'] == 'BUY' else '卖出',
|
||||
'成交量状态': '放量' if row['volume_status'] == 'HIGH' else '正常',
|
||||
'投资建议': row['recommendation']
|
||||
})
|
||||
return formatted_results
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"全盘扫描失败:{str(e)}")
|
||||
raise
|
||||
|
||||
# -------------------------------
|
||||
# **结果分组与报告生成**
|
||||
# -------------------------------
|
||||
def format_price_category(price: float) -> str:
|
||||
"""将价格划分为区间(例如 32.5 -> '30-40')"""
|
||||
base = (price // 10) * 10
|
||||
return f"{int(base)}-{int(base+10)}"
|
||||
|
||||
def save_results_by_price(results: List[Dict]) -> None:
|
||||
"""按价格区间保存分析结果至文件"""
|
||||
try:
|
||||
os.makedirs('scanner', exist_ok=True)
|
||||
price_groups = {}
|
||||
for stock in results:
|
||||
price = float(stock['当前价格'].replace('¥', ''))
|
||||
category = format_price_category(price)
|
||||
price_groups.setdefault(category, []).append(stock)
|
||||
|
||||
for category, stocks in price_groups.items():
|
||||
output_lines = [
|
||||
"=" * 80,
|
||||
f"股票分析结果 - 价格区间: {category}元",
|
||||
f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
"=" * 80,
|
||||
f"\n该区间共发现 {len(stocks)} 支高分股票(得分≥85):",
|
||||
"-" * 80
|
||||
]
|
||||
stocks.sort(key=lambda x: float(x['评分']), reverse=True)
|
||||
for i, stock in enumerate(stocks, 1):
|
||||
output_lines.extend([
|
||||
f"\n{i}. 股票代码: {stock['股票代码']}",
|
||||
f" 评分: {stock['评分']} | 价格: {stock['当前价格']} | 涨跌幅: {stock['涨跌幅']}",
|
||||
f" RSI指标: {stock['RSI指标']} | 均线趋势: {stock['均线趋势']} | MACD信号: {stock['MACD信号']}",
|
||||
f" 成交量状态: {stock['成交量状态']}",
|
||||
f" 投资建议: {stock['投资建议']}",
|
||||
"-" * 80
|
||||
])
|
||||
output_lines.extend([
|
||||
f"\n价格区间 {category}元 分析汇总:",
|
||||
f"1. 股票数量: {len(stocks)}",
|
||||
f"2. 平均评分: {np.mean([float(stock['评分']) for stock in stocks]):.1f}",
|
||||
f"3. 买入信号股票数: {sum(1 for stock in stocks if stock['MACD信号'] == '买入')}",
|
||||
f"4. 放量股票数: {sum(1 for stock in stocks if stock['成交量状态'] == '放量')}"
|
||||
])
|
||||
|
||||
filename = f'scanner/price_{category.replace("-", "_")}.txt'
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(output_lines))
|
||||
create_summary_file(price_groups)
|
||||
except Exception as e:
|
||||
logging.error(f"保存结果时发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def create_summary_file(price_groups: Dict[str, List[Dict]]) -> None:
|
||||
"""生成综合汇总报告"""
|
||||
try:
|
||||
output_lines = [
|
||||
"=" * 80,
|
||||
"A股市场优质股票筛选报告",
|
||||
f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
"=" * 80
|
||||
]
|
||||
total_stocks = sum(len(stocks) for stocks in price_groups.values())
|
||||
all_scores = [float(stock['评分']) for stocks in price_groups.values() for stock in stocks]
|
||||
|
||||
output_lines.extend([
|
||||
"\n整体统计:",
|
||||
f"1. 共筛选出 {total_stocks} 支高分股票(得分≥85)",
|
||||
f"2. 平均评分: {np.mean(all_scores):.1f}",
|
||||
f"3. 最高评分: {max(all_scores):.1f}",
|
||||
"\n各价格区间分布:",
|
||||
"-" * 80
|
||||
])
|
||||
for category, stocks in sorted(price_groups.items(), key=lambda x: float(x[0].split('-')[0])):
|
||||
output_lines.extend([
|
||||
f"\n价格区间 {category}元:",
|
||||
f" - 股票数量: {len(stocks)}",
|
||||
f" - 平均评分: {np.mean([float(stock['评分']) for stock in stocks]):.1f}"
|
||||
])
|
||||
|
||||
with open('scanner/summary.txt', 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(output_lines))
|
||||
except Exception as e:
|
||||
logging.error(f"生成汇总报告失败:{str(e)}")
|
||||
raise
|
||||
|
||||
# -------------------------------
|
||||
# **主程序入口**
|
||||
# -------------------------------
|
||||
def main():
|
||||
"""程序主入口"""
|
||||
print("\n" + "=" * 80)
|
||||
print("Market-Wide High-Score Stock Scanner".center(76))
|
||||
print("=" * 80)
|
||||
|
||||
scanner = TopStockScanner(max_workers=20) # 已提升至20线程
|
||||
try:
|
||||
print("\n开始全盘扫描股票……")
|
||||
high_score_stocks = scanner.get_high_score_stocks(batch_size=20)
|
||||
if not high_score_stocks:
|
||||
print("\n未找到得分大于等于85分的股票。")
|
||||
return
|
||||
|
||||
save_results_by_price(high_score_stocks)
|
||||
|
||||
print(f"\n分析完成!结果已保存至 scanner 文件夹中:")
|
||||
print("1. 按价格区间保存的详细分析文件(price_XX_YY.txt)")
|
||||
print("2. 汇总报告(summary.txt)")
|
||||
|
||||
temp_file = 'scanner/temp_results.txt'
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
input("\n按Enter键退出……")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"\n程序错误:{str(e)}\n"
|
||||
print("=" * 80)
|
||||
print(error_msg)
|
||||
print("=" * 80)
|
||||
os.makedirs('scanner', exist_ok=True)
|
||||
with open('scanner/error_log.txt', 'w', encoding='utf-8') as f:
|
||||
f.write("Stock Analysis System Error Report\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"Error: {str(e)}\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write(f"详细堆栈信息:\n{traceback.format_exc()}")
|
||||
print("错误日志已保存至 scanner/error_log.txt")
|
||||
input("\n按Enter键退出……")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user