From 9ce784748472e21b9fb899cca2c6beb392f4167d Mon Sep 17 00:00:00 2001 From: CaasianVale <1544257291@qq.com> Date: Tue, 4 Mar 2025 19:27:42 +0800 Subject: [PATCH 1/3] Update .gitignore --- .gitignore | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index addb0a7..0d9d47a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,184 @@ +# log +logs/ *.log -. * \ No newline at end of file +. * + +# Custom +.vs/ +.vscode/ +.idea/ +.conda/ +.vs/ +.vscode/ +.idea/ +AppData/ +output/ +dist/ +main.build/ +main.dist/ +main.onefile-build/ +build_upload.log +*report.xml +*.spec +*.zip + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file From 6326157164e1407d0aefe32956f1e13c0252bfb0 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Wed, 5 Mar 2025 10:51:59 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20A=E8=82=A1=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=A0=A1=E9=AA=8C=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stock_analyzer.py | 192 +++++++++++++++++++++++++++++++++---------- templates/index.html | 48 ++++++++++- web_server.py | 2 +- 3 files changed, 195 insertions(+), 47 deletions(-) diff --git a/stock_analyzer.py b/stock_analyzer.py index bcf9375..39adb56 100644 --- a/stock_analyzer.py +++ b/stock_analyzer.py @@ -47,6 +47,25 @@ class StockAnalyzer: end_date = datetime.now().strftime('%Y%m%d') try: + # 验证股票代码格式 + if market_type == 'A': + # 上海证券交易所股票代码以6开头 + # 深圳证券交易所股票代码以0或3开头 + # 科创板股票代码以688开头 + # 北京证券交易所股票代码以8开头 + valid_prefixes = ['0', '3', '6', '688', '8'] + valid_format = False + + for prefix in valid_prefixes: + if stock_code.startswith(prefix): + valid_format = True + break + + if not valid_format: + error_msg = f"无效的A股股票代码格式: {stock_code}。A股代码应以0、3、6、688或8开头" + logger.error(f"[股票代码格式错误] {error_msg}") + raise ValueError(error_msg) + # 根据市场类型获取数据 if market_type == 'A': df = ak.stock_zh_a_hist( @@ -97,7 +116,12 @@ class StockAnalyzer: return df.sort_values('date') + # except ValueError as ve: + # # 捕获格式验证错误 + # logger.error(f"[股票代码格式错误] {str(ve)}") + # raise Exception(f"股票代码格式错误: {str(ve)}") except Exception as e: + logger.error(f"[获取股票数据失败] {str(e)}") raise Exception(f"获取股票数据失败: {str(e)}") def calculate_ema(self, series, period): @@ -252,12 +276,12 @@ class StockAnalyzer: # 检查API配置 if not self.API_URL: error_msg = "API URL未配置,无法进行AI分析" - logger.error(error_msg) + logger.error(f"[API配置错误] {error_msg}") return error_msg if not stream else (yield json.dumps({"error": error_msg})) if not self.API_KEY: error_msg = "API Key未配置,无法进行AI分析" - logger.error(error_msg) + logger.error(f"[API配置错误] {error_msg}") return error_msg if not stream else (yield json.dumps({"error": error_msg})) # 标准化API URL @@ -306,12 +330,12 @@ class StockAnalyzer: error_text = response.text[:500] if response.text else "无响应内容" error_msg = f"API请求失败: 状态码 {response.status_code}, 响应: {error_text}" - logger.error(error_msg) + logger.error(f"[API请求失败] {error_msg}") yield json.dumps({"stock_code": stock_code, "error": error_msg}) except Exception as e: error_msg = f"流式API请求异常: {str(e)}" - logger.error(error_msg) + logger.error(f"[流式API异常] {error_msg}") logger.exception(e) yield json.dumps({"stock_code": stock_code, "error": error_msg}) else: @@ -342,18 +366,18 @@ class StockAnalyzer: error_text = response.text[:500] if response.text else "无响应内容" error_msg = f"API请求失败: 状态码 {response.status_code}, 响应: {error_text}" - logger.error(error_msg) + logger.error(f"[API请求失败] {error_msg}") return error_msg except Exception as e: error_msg = f"非流式API请求异常: {str(e)}" - logger.error(error_msg) + logger.error(f"[非流式API异常] {error_msg}") logger.exception(e) return error_msg except Exception as e: error_msg = f"AI 分析过程中发生错误: {str(e)}" - logger.error(error_msg) + logger.error(f"[AI分析异常] {error_msg}") logger.exception(e) if stream: @@ -422,7 +446,7 @@ class StockAnalyzer: }) yield chunk_json except json.JSONDecodeError as e: - logger.error(f"JSON解析错误: {str(e)}, 行内容: {data_content}") + logger.error(f"[JSON解析错误] {str(e)}, 行内容: {data_content}") # 忽略无法解析的JSON pass else: @@ -437,7 +461,7 @@ class StockAnalyzer: except Exception as e: error_msg = f"处理AI流式响应时出错: {str(e)}" - logger.error(error_msg) + logger.error(f"[流式响应异常] {error_msg}") logger.exception(e) yield json.dumps({"stock_code": stock_code, "error": error_msg}) @@ -457,13 +481,48 @@ class StockAnalyzer: return '强烈建议卖出' def analyze_stock(self, stock_code, market_type='A', stream=False): - """分析单个股票""" + """分析单只股票""" + logger.info(f"开始分析股票 {stock_code}, 市场类型: {market_type}, 流式模式: {stream}") + try: - logger.info(f"开始分析股票: {stock_code}, 市场: {market_type}, 流式模式: {stream}") - # 获取股票数据 - logger.debug(f"获取股票 {stock_code} 数据") - df = self.get_stock_data(stock_code, market_type) + try: + df = self.get_stock_data(stock_code, market_type) + except Exception as e: + # 捕获股票数据获取异常 + error_msg = str(e) + logger.error(f"[数据获取异常] {error_msg}") + + # 格式化错误响应 + error_response = { + 'stock_code': stock_code, + 'error': error_msg, + 'status': 'error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + if stream: + return (yield json.dumps(error_response)) + else: + return error_response + + # 检查数据是否为空 + if df.empty: + error_msg = f"股票 {stock_code} 数据为空" + logger.error(f"[空数据] {error_msg}") + + # 格式化错误响应 + error_response = { + 'stock_code': stock_code, + 'error': error_msg, + 'status': 'error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + if stream: + return (yield json.dumps(error_response)) + else: + return error_response # 计算技术指标 logger.debug(f"计算股票 {stock_code} 技术指标") @@ -518,61 +577,108 @@ class StockAnalyzer: return report except Exception as e: - error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" - logger.error(error_msg) + error_msg = f"分析股票 {stock_code} 时出错: {str(e)}\n" + logger.error(f"[股票分析异常] {error_msg}") logger.exception(e) - if stream: - error_json = json.dumps({'stock_code': stock_code, 'error': error_msg}) - logger.info(f"流式错误输出: {error_json}") - yield error_json - else: - raise + # 格式化错误响应 + error_response = { + 'stock_code': stock_code, + 'error': error_msg, + 'status': 'error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } - def scan_market(self, stock_list, min_score=60, market_type='A', stream=False): - """扫描市场,寻找符合条件的股票""" - logger.info(f"开始扫描市场,股票数量: {len(stock_list)}, 最低分数: {min_score}, 市场: {market_type}, 流式模式: {stream}") + if stream: + return (yield json.dumps(error_response)) + else: + return error_response + + def scan_stocks(self, stock_codes, market_type='A', min_score=60, stream=False): + """扫描多只股票""" + logger.info(f"开始扫描 {len(stock_codes)} 只股票, 市场类型: {market_type}, 最低评分: {min_score}, 流式模式: {stream}") if not stream: - recommendations = [] + # 非流式模式 + recommended_stocks = [] + stock_count = 0 + error_count = 0 - for stock_code in stock_list: + for stock_code in stock_codes: + stock_count += 1 + logger.info(f"扫描进度: {stock_count}/{len(stock_codes)}, 当前股票: {stock_code}") + try: - logger.debug(f"分析股票: {stock_code}") report = self.analyze_stock(stock_code, market_type) + + # 检查是否有错误 + if isinstance(report, dict) and 'error' in report: + error_count += 1 + logger.warning(f"[扫描错误] 股票 {stock_code}: {report['error']}") + continue + + # 检查评分是否达到最低要求 if report['score'] >= min_score: logger.info(f"股票 {stock_code} 评分 {report['score']} >= {min_score},添加到推荐列表") - recommendations.append(report) + recommended_stocks.append(report) else: logger.debug(f"股票 {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表") except Exception as e: - logger.error(f"分析股票 {stock_code} 时出错: {str(e)}") + error_count += 1 + error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" + logger.error(f"[扫描异常] {error_msg}") logger.exception(e) - continue - # 按得分排序 - recommendations.sort(key=lambda x: x['score'], reverse=True) - logger.info(f"扫描完成,找到 {len(recommendations)} 个推荐股票") - return recommendations + # 添加错误信息到推荐列表,确保前端能看到错误 + error_response = { + 'stock_code': stock_code, + 'error': error_msg, + 'status': 'error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + recommended_stocks.append(error_response) + continue + + logger.info(f"扫描完成,共 {stock_count} 只股票,{error_count} 只出错,{len(recommended_stocks)} 只推荐") + return recommended_stocks else: - # 流式处理每个股票 - logger.info(f"开始流式扫描 {len(stock_list)} 只股票") + # 流式模式 stock_count = 0 - for stock_code in stock_list: + error_count = 0 + + for stock_code in stock_codes: stock_count += 1 - logger.debug(f"流式分析股票 {stock_code} ({stock_count}/{len(stock_list)})") + logger.info(f"流式扫描进度: {stock_count}/{len(stock_codes)}, 当前股票: {stock_code}") + try: - # 分析单只股票并获取流式结果 chunk_count = 0 for chunk in self.analyze_stock(stock_code, market_type, stream=True): chunk_count += 1 + # 检查是否有错误信息 + try: + chunk_data = json.loads(chunk) + if 'error' in chunk_data: + error_count += 1 + logger.warning(f"[流式扫描错误] 股票 {stock_code}: {chunk_data['error']}") + except: + pass yield chunk logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块") except Exception as e: + error_count += 1 error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" - logger.error(error_msg) + logger.error(f"[流式扫描异常] {error_msg}") logger.exception(e) - error_json = json.dumps({'stock_code': stock_code, 'error': error_msg}) + + # 格式化错误响应 + error_response = { + 'stock_code': stock_code, + 'error': error_msg, + 'status': 'error', + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + error_json = json.dumps(error_response) logger.info(f"流式错误输出: {error_json}") yield error_json - logger.info(f"流式扫描完成,处理了 {stock_count} 只股票") + + logger.info(f"流式扫描完成,共处理 {stock_count} 只股票,{error_count} 只出错") diff --git a/templates/index.html b/templates/index.html index eeeeb61..45ebb39 100644 --- a/templates/index.html +++ b/templates/index.html @@ -665,14 +665,56 @@ if (chunk.error) { // 添加或更新显示错误的卡片 let errorCard = document.getElementById(`error-${stockCode}`); + + // 处理错误信息,提取关键部分 + let errorMessage = chunk.error; + + // 移除多余的错误前缀 + errorMessage = errorMessage.replace(/获取股票数据失败: /g, ''); + + // 格式化错误信息 + let formattedError = errorMessage; + + // 针对特定类型的错误提供更友好的提示 + if (errorMessage.includes('无效的A股股票代码格式')) { + formattedError = `股票代码格式错误: ${stockCode} 不是有效的A股代码
+ A股代码应以0、3、6、688或8开头`; + } else if (errorMessage.includes('股票代码格式错误')) { + formattedError = `股票代码格式错误: ${errorMessage.split(':')[1] || errorMessage}`; + } else if (errorMessage.includes('数据为空')) { + formattedError = `数据获取失败: 未找到股票 ${stockCode} 的交易数据`; + } + if (!errorCard) { errorCard = document.createElement('div'); errorCard.id = `error-${stockCode}`; - errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600'; - errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`; + errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600 mb-4 flex items-start'; + + // 添加警告图标 + errorCard.innerHTML = ` +
+ + + +
+
+

股票 ${stockCode} 分析失败

+

${formattedError}

+
+ `; container.appendChild(errorCard); } else { - errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`; + errorCard.innerHTML = ` +
+ + + +
+
+

股票 ${stockCode} 分析失败

+

${formattedError}

+
+ `; } return; } diff --git a/web_server.py b/web_server.py index b0ef986..902734a 100644 --- a/web_server.py +++ b/web_server.py @@ -88,7 +88,7 @@ def analyze(): logger.debug(f"开始处理批量股票的流式响应") chunk_count = 0 - for chunk in custom_analyzer.scan_market( + for chunk in custom_analyzer.scan_stocks( [code.strip() for code in stock_codes], min_score=0, market_type=market_type, From cdd2728e71e6e02f9f303ca14bcabe99c162ff18 Mon Sep 17 00:00:00 2001 From: Cassianvale Date: Wed, 5 Mar 2025 11:09:09 +0800 Subject: [PATCH 3/3] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8ee6077..db3d017 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # log logs/ *.log -. * # Custom .vs/