diff --git a/.gitignore b/.gitignore index 399ad79..db3d017 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,183 @@ -logs/* -.* \ No newline at end of file +# log +logs/ +*.log + +# 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/ diff --git a/stock_analyzer.py b/stock_analyzer.py index a1fd87c..1d92aaa 100644 --- a/stock_analyzer.py +++ b/stock_analyzer.py @@ -47,8 +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) + df = ak.stock_zh_a_hist( symbol=stock_code, start_date=start_date, @@ -108,8 +125,13 @@ 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: - raise Exception(f"获取股票数据失败: {str(e)}") + logger.error(f"[获取数据失败] {str(e)}") + raise Exception(f"获取数据失败: {str(e)}") def calculate_ema(self, series, period): """计算指数移动平均线""" @@ -192,7 +214,7 @@ class StockAnalyzer: raise def calculate_score(self, df): - """计算股票评分""" + """计算评分""" try: score = 0 latest = df.iloc[-1] @@ -325,12 +347,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 @@ -379,12 +401,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: @@ -415,18 +437,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: @@ -495,7 +517,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: @@ -510,7 +532,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}) @@ -530,13 +552,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}") + # 获取股票数据 + 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 - # 获取数据 - logger.debug(f"获取 {stock_code} 数据") - df = self.get_stock_data(stock_code, market_type) + # 检查数据是否为空 + 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} 技术指标") @@ -592,61 +649,109 @@ 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 86b7295..0f159c7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -724,14 +724,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 8d4017f..2e13be7 100644 --- a/web_server.py +++ b/web_server.py @@ -90,7 +90,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,