Merge branch 'pr-1' into dev
# Conflicts: # stock_analyzer.py # templates/index.html
This commit is contained in:
185
.gitignore
vendored
185
.gitignore
vendored
@@ -1,2 +1,183 @@
|
|||||||
logs/*
|
# 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/
|
||||||
|
|||||||
@@ -47,8 +47,25 @@ class StockAnalyzer:
|
|||||||
end_date = datetime.now().strftime('%Y%m%d')
|
end_date = datetime.now().strftime('%Y%m%d')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 根据市场类型获取数据
|
# 验证股票代码格式
|
||||||
if market_type == 'A':
|
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(
|
df = ak.stock_zh_a_hist(
|
||||||
symbol=stock_code,
|
symbol=stock_code,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
@@ -108,8 +125,13 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
return df.sort_values('date')
|
return df.sort_values('date')
|
||||||
|
|
||||||
|
# except ValueError as ve:
|
||||||
|
# # 捕获格式验证错误
|
||||||
|
# logger.error(f"[股票代码格式错误] {str(ve)}")
|
||||||
|
# raise Exception(f"股票代码格式错误: {str(ve)}")
|
||||||
except Exception as e:
|
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):
|
def calculate_ema(self, series, period):
|
||||||
"""计算指数移动平均线"""
|
"""计算指数移动平均线"""
|
||||||
@@ -192,7 +214,7 @@ class StockAnalyzer:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def calculate_score(self, df):
|
def calculate_score(self, df):
|
||||||
"""计算股票评分"""
|
"""计算评分"""
|
||||||
try:
|
try:
|
||||||
score = 0
|
score = 0
|
||||||
latest = df.iloc[-1]
|
latest = df.iloc[-1]
|
||||||
@@ -325,12 +347,12 @@ class StockAnalyzer:
|
|||||||
# 检查API配置
|
# 检查API配置
|
||||||
if not self.API_URL:
|
if not self.API_URL:
|
||||||
error_msg = "API URL未配置,无法进行AI分析"
|
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}))
|
return error_msg if not stream else (yield json.dumps({"error": error_msg}))
|
||||||
|
|
||||||
if not self.API_KEY:
|
if not self.API_KEY:
|
||||||
error_msg = "API Key未配置,无法进行AI分析"
|
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}))
|
return error_msg if not stream else (yield json.dumps({"error": error_msg}))
|
||||||
|
|
||||||
# 标准化API URL
|
# 标准化API URL
|
||||||
@@ -379,12 +401,12 @@ class StockAnalyzer:
|
|||||||
error_text = response.text[:500] if response.text else "无响应内容"
|
error_text = response.text[:500] if response.text else "无响应内容"
|
||||||
|
|
||||||
error_msg = f"API请求失败: 状态码 {response.status_code}, 响应: {error_text}"
|
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})
|
yield json.dumps({"stock_code": stock_code, "error": error_msg})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"流式API请求异常: {str(e)}"
|
error_msg = f"流式API请求异常: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[流式API异常] {error_msg}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
yield json.dumps({"stock_code": stock_code, "error": error_msg})
|
yield json.dumps({"stock_code": stock_code, "error": error_msg})
|
||||||
else:
|
else:
|
||||||
@@ -415,18 +437,18 @@ class StockAnalyzer:
|
|||||||
error_text = response.text[:500] if response.text else "无响应内容"
|
error_text = response.text[:500] if response.text else "无响应内容"
|
||||||
|
|
||||||
error_msg = f"API请求失败: 状态码 {response.status_code}, 响应: {error_text}"
|
error_msg = f"API请求失败: 状态码 {response.status_code}, 响应: {error_text}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[API请求失败] {error_msg}")
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"非流式API请求异常: {str(e)}"
|
error_msg = f"非流式API请求异常: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[非流式API异常] {error_msg}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"AI 分析过程中发生错误: {str(e)}"
|
error_msg = f"AI 分析过程中发生错误: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[AI分析异常] {error_msg}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
@@ -495,7 +517,7 @@ class StockAnalyzer:
|
|||||||
})
|
})
|
||||||
yield chunk_json
|
yield chunk_json
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"JSON解析错误: {str(e)}, 行内容: {data_content}")
|
logger.error(f"[JSON解析错误] {str(e)}, 行内容: {data_content}")
|
||||||
# 忽略无法解析的JSON
|
# 忽略无法解析的JSON
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -510,7 +532,7 @@ class StockAnalyzer:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"处理AI流式响应时出错: {str(e)}"
|
error_msg = f"处理AI流式响应时出错: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[流式响应异常] {error_msg}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
yield json.dumps({"stock_code": stock_code, "error": error_msg})
|
yield json.dumps({"stock_code": stock_code, "error": error_msg})
|
||||||
|
|
||||||
@@ -530,13 +552,48 @@ class StockAnalyzer:
|
|||||||
return '强烈建议卖出'
|
return '强烈建议卖出'
|
||||||
|
|
||||||
def analyze_stock(self, stock_code, market_type='A', stream=False):
|
def analyze_stock(self, stock_code, market_type='A', stream=False):
|
||||||
"""分析股票或基金"""
|
"""分析单只"""
|
||||||
try:
|
logger.info(f"开始分析 {stock_code}, 市场类型: {market_type}, 流式模式: {stream}")
|
||||||
logger.info(f"开始分析: {stock_code}, 市场: {market_type}, 流式模式: {stream}")
|
|
||||||
|
|
||||||
# 获取数据
|
try:
|
||||||
logger.debug(f"获取 {stock_code} 数据")
|
# 获取股票数据
|
||||||
|
try:
|
||||||
df = self.get_stock_data(stock_code, market_type)
|
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} 技术指标")
|
logger.debug(f"计算 {stock_code} 技术指标")
|
||||||
@@ -592,61 +649,109 @@ class StockAnalyzer:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"分析 {stock_code} 时出错: {str(e)}"
|
error_msg = f"分析 {stock_code} 时出错: {str(e)}\n"
|
||||||
logger.error(error_msg)
|
logger.error(f"[分析异常] {error_msg}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
if stream:
|
# 格式化错误响应
|
||||||
error_json = json.dumps({'stock_code': stock_code, 'error': error_msg})
|
error_response = {
|
||||||
logger.info(f"流式错误输出: {error_json}")
|
'stock_code': stock_code,
|
||||||
yield error_json
|
'error': error_msg,
|
||||||
else:
|
'status': 'error',
|
||||||
raise
|
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}
|
||||||
|
|
||||||
def scan_market(self, stock_list, min_score=60, market_type='A', stream=False):
|
if stream:
|
||||||
"""扫描市场,寻找符合条件的"""
|
return (yield json.dumps(error_response))
|
||||||
logger.info(f"开始扫描市场,数量: {len(stock_list)}, 最低分数: {min_score}, 市场: {market_type}, 流式模式: {stream}")
|
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:
|
if not stream:
|
||||||
recommendations = []
|
# 非流式模式
|
||||||
|
recommended_stocks = []
|
||||||
|
stock_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for stock_code in stock_codes:
|
||||||
|
stock_count += 1
|
||||||
|
logger.info(f"扫描进度: {stock_count}/{len(stock_codes)}, 当前: {stock_code}")
|
||||||
|
|
||||||
for stock_code in stock_list:
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"分析: {stock_code}")
|
logger.debug(f"分析: {stock_code}")
|
||||||
report = self.analyze_stock(stock_code, market_type)
|
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:
|
if report['score'] >= min_score:
|
||||||
logger.info(f" {stock_code} 评分 {report['score']} >= {min_score},添加到推荐列表")
|
logger.info(f" {stock_code} 评分 {report['score']} >= {min_score},添加到推荐列表")
|
||||||
recommendations.append(report)
|
recommended_stocks.append(report)
|
||||||
else:
|
else:
|
||||||
logger.debug(f" {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表")
|
logger.debug(f" {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表")
|
||||||
except Exception as e:
|
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)
|
logger.exception(e)
|
||||||
|
|
||||||
|
# 添加错误信息到推荐列表,确保前端能看到错误
|
||||||
|
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
|
continue
|
||||||
|
|
||||||
# 按得分排序
|
logger.info(f"扫描完成,共 {stock_count} 只,{error_count} 只出错,{len(recommended_stocks)} 只推荐")
|
||||||
recommendations.sort(key=lambda x: x['score'], reverse=True)
|
return recommended_stocks
|
||||||
logger.info(f"扫描完成,找到 {len(recommendations)} 个推荐股票")
|
|
||||||
return recommendations
|
|
||||||
else:
|
else:
|
||||||
# 流式处理每个股票
|
# 流式模式
|
||||||
logger.info(f"开始流式扫描 {len(stock_list)} 只股票")
|
|
||||||
stock_count = 0
|
stock_count = 0
|
||||||
for stock_code in stock_list:
|
error_count = 0
|
||||||
|
|
||||||
|
for stock_code in stock_codes:
|
||||||
stock_count += 1
|
stock_count += 1
|
||||||
logger.debug(f"流式分析 {stock_code} ({stock_count}/{len(stock_list)})")
|
logger.info(f"流式扫描进度: {stock_count}/{len(stock_codes)}, 当前: {stock_code}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 分析单只股票并获取流式结果
|
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
for chunk in self.analyze_stock(stock_code, market_type, stream=True):
|
for chunk in self.analyze_stock(stock_code, market_type, stream=True):
|
||||||
chunk_count += 1
|
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
|
yield chunk
|
||||||
logger.debug(f" {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
logger.debug(f" {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
error_msg = f"分析 {stock_code} 时出错: {str(e)}"
|
error_msg = f"分析 {stock_code} 时出错: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[流式扫描异常] {error_msg}")
|
||||||
logger.exception(e)
|
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}")
|
logger.info(f"流式错误输出: {error_json}")
|
||||||
yield error_json
|
yield error_json
|
||||||
logger.info(f"流式扫描完成,处理了 {stock_count} 只股票")
|
|
||||||
|
logger.info(f"流式扫描完成,共处理 {stock_count} ,{error_count} 只出错")
|
||||||
|
|||||||
@@ -724,14 +724,56 @@
|
|||||||
if (chunk.error) {
|
if (chunk.error) {
|
||||||
// 添加或更新显示错误的卡片
|
// 添加或更新显示错误的卡片
|
||||||
let errorCard = document.getElementById(`error-${stockCode}`);
|
let errorCard = document.getElementById(`error-${stockCode}`);
|
||||||
|
|
||||||
|
// 处理错误信息,提取关键部分
|
||||||
|
let errorMessage = chunk.error;
|
||||||
|
|
||||||
|
// 移除多余的错误前缀
|
||||||
|
errorMessage = errorMessage.replace(/获取股票数据失败: /g, '');
|
||||||
|
|
||||||
|
// 格式化错误信息
|
||||||
|
let formattedError = errorMessage;
|
||||||
|
|
||||||
|
// 针对特定类型的错误提供更友好的提示
|
||||||
|
if (errorMessage.includes('无效的A股股票代码格式')) {
|
||||||
|
formattedError = `<strong>股票代码格式错误</strong>: ${stockCode} 不是有效的A股代码<br>
|
||||||
|
<small>A股代码应以0、3、6、688或8开头</small>`;
|
||||||
|
} else if (errorMessage.includes('股票代码格式错误')) {
|
||||||
|
formattedError = `<strong>股票代码格式错误</strong>: ${errorMessage.split(':')[1] || errorMessage}`;
|
||||||
|
} else if (errorMessage.includes('数据为空')) {
|
||||||
|
formattedError = `<strong>数据获取失败</strong>: 未找到股票 ${stockCode} 的交易数据`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!errorCard) {
|
if (!errorCard) {
|
||||||
errorCard = document.createElement('div');
|
errorCard = document.createElement('div');
|
||||||
errorCard.id = `error-${stockCode}`;
|
errorCard.id = `error-${stockCode}`;
|
||||||
errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600';
|
errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600 mb-4 flex items-start';
|
||||||
errorCard.innerHTML = `分析 ${stockCode} 出错: ${chunk.error}`;
|
|
||||||
|
// 添加警告图标
|
||||||
|
errorCard.innerHTML = `
|
||||||
|
<div class="mr-3 flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">股票 ${stockCode} 分析失败</p>
|
||||||
|
<p class="mt-1 text-sm">${formattedError}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
container.appendChild(errorCard);
|
container.appendChild(errorCard);
|
||||||
} else {
|
} else {
|
||||||
errorCard.innerHTML = `分析 ${stockCode} 出错: ${chunk.error}`;
|
errorCard.innerHTML = `
|
||||||
|
<div class="mr-3 flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">股票 ${stockCode} 分析失败</p>
|
||||||
|
<p class="mt-1 text-sm">${formattedError}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def analyze():
|
|||||||
|
|
||||||
logger.debug(f"开始处理批量股票的流式响应")
|
logger.debug(f"开始处理批量股票的流式响应")
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
for chunk in custom_analyzer.scan_market(
|
for chunk in custom_analyzer.scan_stocks(
|
||||||
[code.strip() for code in stock_codes],
|
[code.strip() for code in stock_codes],
|
||||||
min_score=0,
|
min_score=0,
|
||||||
market_type=market_type,
|
market_type=market_type,
|
||||||
|
|||||||
Reference in New Issue
Block a user