修复一些小bug
This commit is contained in:
+7
-1
@@ -1,10 +1,13 @@
|
|||||||
# 使用官方Python镜像作为基础镜像
|
# 使用官方Python镜像作为基础镜像
|
||||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim
|
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim
|
||||||
|
|
||||||
|
# 设置时区环境变量
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装Node.js (pywencai需要) 和中文字体 (PDF生成需要)
|
# 安装Node.js (pywencai需要)、中文字体 (PDF生成需要) 和时区数据
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
gnupg \
|
gnupg \
|
||||||
@@ -12,6 +15,9 @@ RUN apt-get update && apt-get install -y \
|
|||||||
fonts-wqy-zenhei \
|
fonts-wqy-zenhei \
|
||||||
fonts-wqy-microhei \
|
fonts-wqy-microhei \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
|
tzdata \
|
||||||
|
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||||
|
&& echo $TZ > /etc/timezone \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& apt-get install -y nodejs \
|
&& apt-get install -y nodejs \
|
||||||
&& fc-cache -fv \
|
&& fc-cache -fv \
|
||||||
|
|||||||
+21
-15
@@ -1,28 +1,34 @@
|
|||||||
# 使用官方Python镜像作为基础镜像
|
# 使用官方Python镜像作为基础镜像
|
||||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim
|
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim
|
||||||
|
|
||||||
|
# 设置时区环境变量
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
# 替换apt-get源为国内源(阿里源)
|
# 替换apt-get源为国内源(阿里源)
|
||||||
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main" > /etc/apt/sources.list && \
|
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main" > /etc/apt/sources.list && \
|
||||||
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list && \
|
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list && \
|
||||||
echo "deb https://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list && \
|
echo "deb https://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list && \
|
||||||
rm -rf /etc/apt/sources.list.d/* || true
|
rm -rf /etc/apt/sources.list.d/* || true
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装Node.js和中文字体 - 使用修正版方案一
|
# 安装Node.js、中文字体和时区数据 - 使用修正版方案一
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
gnupg \
|
gnupg \
|
||||||
fonts-noto-cjk \
|
fonts-noto-cjk \
|
||||||
fonts-wqy-zenhei \
|
fonts-wqy-zenhei \
|
||||||
fonts-wqy-microhei \
|
fonts-wqy-microhei \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
&& curl -fsSL https://mirrors.aliyun.com/nodesource/setup_18.x | bash - \
|
tzdata \
|
||||||
&& apt-get install -y nodejs npm \
|
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||||
&& fc-cache -fv \
|
&& echo $TZ > /etc/timezone \
|
||||||
&& apt-get clean \
|
&& curl -fsSL https://mirrors.aliyun.com/nodesource/setup_18.x | bash - \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& apt-get install -y nodejs npm \
|
||||||
|
&& fc-cache -fv \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 验证安装 - 添加详细检查
|
# 验证安装 - 添加详细检查
|
||||||
RUN echo "Node.js版本:" && node --version && \
|
RUN echo "Node.js版本:" && node --version && \
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ def main():
|
|||||||
# 快捷导航 - 移到顶部
|
# 快捷导航 - 移到顶部
|
||||||
st.markdown("### 🔍 快捷导航")
|
st.markdown("### 🔍 快捷导航")
|
||||||
|
|
||||||
if st.button("📖 历史记录", use_container_width=True, key="nav_history"):
|
if st.button("📖 历史记录", width='stretch', key="nav_history"):
|
||||||
st.session_state.show_history = True
|
st.session_state.show_history = True
|
||||||
if 'show_monitor' in st.session_state:
|
if 'show_monitor' in st.session_state:
|
||||||
del st.session_state.show_monitor
|
del st.session_state.show_monitor
|
||||||
@@ -299,7 +299,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("📊 实时监测", use_container_width=True, key="nav_monitor"):
|
if st.button("📊 实时监测", width='stretch', key="nav_monitor"):
|
||||||
st.session_state.show_monitor = True
|
st.session_state.show_monitor = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -310,7 +310,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("🎯 主力选股", use_container_width=True, key="nav_main_force"):
|
if st.button("🎯 主力选股", width='stretch', key="nav_main_force"):
|
||||||
st.session_state.show_main_force = True
|
st.session_state.show_main_force = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -325,7 +325,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("🎯 智策板块", use_container_width=True, key="nav_sector_strategy"):
|
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy"):
|
||||||
st.session_state.show_sector_strategy = True
|
st.session_state.show_sector_strategy = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -340,7 +340,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("🎯 智瞰龙虎", use_container_width=True, key="nav_longhubang"):
|
if st.button("🎯 智瞰龙虎", width='stretch', key="nav_longhubang"):
|
||||||
st.session_state.show_longhubang = True
|
st.session_state.show_longhubang = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -355,7 +355,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("📊 持仓分析", use_container_width=True, key="nav_portfolio"):
|
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio"):
|
||||||
st.session_state.show_portfolio = True
|
st.session_state.show_portfolio = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -370,7 +370,7 @@ def main():
|
|||||||
if 'show_longhubang' in st.session_state:
|
if 'show_longhubang' in st.session_state:
|
||||||
del st.session_state.show_longhubang
|
del st.session_state.show_longhubang
|
||||||
|
|
||||||
if st.button("🏠 返回首页", use_container_width=True, key="nav_home"):
|
if st.button("🏠 返回首页", width='stretch', key="nav_home"):
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
if 'show_monitor' in st.session_state:
|
if 'show_monitor' in st.session_state:
|
||||||
@@ -386,7 +386,7 @@ def main():
|
|||||||
if 'show_portfolio' in st.session_state:
|
if 'show_portfolio' in st.session_state:
|
||||||
del st.session_state.show_portfolio
|
del st.session_state.show_portfolio
|
||||||
|
|
||||||
if st.button("⚙️ 环境配置", use_container_width=True, key="nav_config"):
|
if st.button("⚙️ 环境配置", width='stretch', key="nav_config"):
|
||||||
st.session_state.show_config = True
|
st.session_state.show_config = True
|
||||||
if 'show_history' in st.session_state:
|
if 'show_history' in st.session_state:
|
||||||
del st.session_state.show_history
|
del st.session_state.show_history
|
||||||
@@ -546,10 +546,10 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
analyze_button = st.button("🚀 开始分析", type="primary", use_container_width=True)
|
analyze_button = st.button("🚀 开始分析", type="primary", width='stretch')
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
if st.button("🔄 清除缓存", use_container_width=True):
|
if st.button("🔄 清除缓存", width='stretch'):
|
||||||
st.cache_data.clear()
|
st.cache_data.clear()
|
||||||
st.success("缓存已清除")
|
st.success("缓存已清除")
|
||||||
|
|
||||||
@@ -564,13 +564,13 @@ def main():
|
|||||||
|
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
with col1:
|
with col1:
|
||||||
analyze_button = st.button("🚀 开始批量分析", type="primary", use_container_width=True)
|
analyze_button = st.button("🚀 开始批量分析", type="primary", width='stretch')
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("🔄 清除缓存", use_container_width=True):
|
if st.button("🔄 清除缓存", width='stretch'):
|
||||||
st.cache_data.clear()
|
st.cache_data.clear()
|
||||||
st.success("缓存已清除")
|
st.success("缓存已清除")
|
||||||
with col3:
|
with col3:
|
||||||
if st.button("🗑️ 清除结果", use_container_width=True):
|
if st.button("🗑️ 清除结果", width='stretch'):
|
||||||
if 'batch_analysis_results' in st.session_state:
|
if 'batch_analysis_results' in st.session_state:
|
||||||
del st.session_state.batch_analysis_results
|
del st.session_state.batch_analysis_results
|
||||||
st.success("已清除批量分析结果")
|
st.success("已清除批量分析结果")
|
||||||
@@ -1450,7 +1450,7 @@ def display_stock_chart(stock_data, stock_info):
|
|||||||
|
|
||||||
# 生成唯一的key
|
# 生成唯一的key
|
||||||
chart_key = f"main_stock_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
|
chart_key = f"main_stock_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
|
||||||
st.plotly_chart(fig, use_container_width=True, key=chart_key)
|
st.plotly_chart(fig, width='stretch', key=chart_key)
|
||||||
|
|
||||||
# 成交量图
|
# 成交量图
|
||||||
if 'Volume' in stock_data.columns:
|
if 'Volume' in stock_data.columns:
|
||||||
@@ -1471,7 +1471,7 @@ def display_stock_chart(stock_data, stock_info):
|
|||||||
|
|
||||||
# 生成唯一的key
|
# 生成唯一的key
|
||||||
volume_key = f"volume_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
|
volume_key = f"volume_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}"
|
||||||
st.plotly_chart(fig_volume, use_container_width=True, key=volume_key)
|
st.plotly_chart(fig_volume, width='stretch', key=volume_key)
|
||||||
|
|
||||||
def display_agents_analysis(agents_results):
|
def display_agents_analysis(agents_results):
|
||||||
"""显示各分析师报告"""
|
"""显示各分析师报告"""
|
||||||
@@ -1841,10 +1841,10 @@ def display_add_to_monitor_dialog(record):
|
|||||||
col_a, col_b, col_c = st.columns(3)
|
col_a, col_b, col_c = st.columns(3)
|
||||||
|
|
||||||
with col_a:
|
with col_a:
|
||||||
submit = st.form_submit_button("✅ 确认加入监测", type="primary", use_container_width=True)
|
submit = st.form_submit_button("✅ 确认加入监测", type="primary", width='stretch')
|
||||||
|
|
||||||
with col_b:
|
with col_b:
|
||||||
cancel = st.form_submit_button("❌ 取消", use_container_width=True)
|
cancel = st.form_submit_button("❌ 取消", width='stretch')
|
||||||
|
|
||||||
if submit:
|
if submit:
|
||||||
if new_entry_min > 0 and new_entry_max > 0 and new_entry_max > new_entry_min:
|
if new_entry_min > 0 and new_entry_max > 0 and new_entry_max > new_entry_min:
|
||||||
@@ -2050,7 +2050,7 @@ def display_record_detail(record_id):
|
|||||||
col1, col2 = st.columns([1, 3])
|
col1, col2 = st.columns([1, 3])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
if st.button("➕ 加入监测", type="primary", use_container_width=True):
|
if st.button("➕ 加入监测", type="primary", width='stretch'):
|
||||||
st.session_state.add_to_monitor_id = record_id
|
st.session_state.add_to_monitor_id = record_id
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
@@ -2362,7 +2362,7 @@ def display_config_manager():
|
|||||||
|
|
||||||
# 测试连通按钮
|
# 测试连通按钮
|
||||||
if new_webhook_enabled and new_webhook_url:
|
if new_webhook_enabled and new_webhook_url:
|
||||||
if st.button("🧪 测试Webhook连通", use_container_width=True, key="test_webhook_btn"):
|
if st.button("🧪 测试Webhook连通", width='stretch', key="test_webhook_btn"):
|
||||||
with st.spinner("正在发送测试消息..."):
|
with st.spinner("正在发送测试消息..."):
|
||||||
# 临时更新配置
|
# 临时更新配置
|
||||||
temp_env_backup = {}
|
temp_env_backup = {}
|
||||||
@@ -2411,7 +2411,7 @@ def display_config_manager():
|
|||||||
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
|
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
if st.button("💾 保存配置", type="primary", use_container_width=True):
|
if st.button("💾 保存配置", type="primary", width='stretch'):
|
||||||
# 验证配置
|
# 验证配置
|
||||||
is_valid, message = config_manager.validate_config(st.session_state.temp_config)
|
is_valid, message = config_manager.validate_config(st.session_state.temp_config)
|
||||||
|
|
||||||
@@ -2436,14 +2436,14 @@ def display_config_manager():
|
|||||||
st.error(f"❌ 配置验证失败: {message}")
|
st.error(f"❌ 配置验证失败: {message}")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("🔄 重置", use_container_width=True):
|
if st.button("🔄 重置", width='stretch'):
|
||||||
# 重置为当前文件中的值
|
# 重置为当前文件中的值
|
||||||
st.session_state.temp_config = {key: info["value"] for key, info in config_info.items()}
|
st.session_state.temp_config = {key: info["value"] for key, info in config_info.items()}
|
||||||
st.success("✅ 已重置为当前配置")
|
st.success("✅ 已重置为当前配置")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
if st.button("⬅️ 返回", use_container_width=True):
|
if st.button("⬅️ 返回", width='stretch'):
|
||||||
if 'show_config' in st.session_state:
|
if 'show_config' in st.session_state:
|
||||||
del st.session_state.show_config
|
del st.session_state.show_config
|
||||||
if 'temp_config' in st.session_state:
|
if 'temp_config' in st.session_state:
|
||||||
@@ -2571,6 +2571,10 @@ def display_comparison_table(results):
|
|||||||
confidence = 'N/A'
|
confidence = 'N/A'
|
||||||
target_price = 'N/A'
|
target_price = 'N/A'
|
||||||
|
|
||||||
|
# 确保信心度为字符串类型,避免类型混合导致的序列化错误
|
||||||
|
if isinstance(confidence, (int, float)):
|
||||||
|
confidence = str(confidence)
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
'股票代码': stock_info.get('symbol', 'N/A'),
|
'股票代码': stock_info.get('symbol', 'N/A'),
|
||||||
'股票名称': stock_info.get('name', 'N/A'),
|
'股票名称': stock_info.get('name', 'N/A'),
|
||||||
@@ -2593,7 +2597,7 @@ def display_comparison_table(results):
|
|||||||
# 显示表格(不使用样式,避免matplotlib导入问题)
|
# 显示表格(不使用样式,避免matplotlib导入问题)
|
||||||
st.dataframe(
|
st.dataframe(
|
||||||
df,
|
df,
|
||||||
use_container_width=True,
|
width='stretch',
|
||||||
height=400
|
height=400
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2633,7 +2637,7 @@ def display_comparison_table(results):
|
|||||||
filtered_df = filtered_df.sort_values('RSI', ascending=False)
|
filtered_df = filtered_df.sort_values('RSI', ascending=False)
|
||||||
|
|
||||||
if not filtered_df.empty:
|
if not filtered_df.empty:
|
||||||
st.dataframe(filtered_df, use_container_width=True)
|
st.dataframe(filtered_df, width='stretch')
|
||||||
else:
|
else:
|
||||||
st.info("没有符合条件的股票")
|
st.info("没有符合条件的股票")
|
||||||
|
|
||||||
|
|||||||
+24
-5
@@ -489,9 +489,14 @@ class PortfolioScheduler:
|
|||||||
|
|
||||||
def _reschedule(self):
|
def _reschedule(self):
|
||||||
"""重新调度任务(支持多个时间点)"""
|
"""重新调度任务(支持多个时间点)"""
|
||||||
schedule.clear()
|
# 只清除持仓定时分析的任务,不影响其他模块
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if not any(tag in ['sector_strategy', 'monitor'] for tag in job.tags)]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
|
||||||
for time_str in self.schedule_times:
|
for time_str in self.schedule_times:
|
||||||
schedule.every().day.at(time_str).do(self._scheduled_job)
|
job = schedule.every().day.at(time_str).do(self._scheduled_job)
|
||||||
|
job.tag('portfolio_analysis')
|
||||||
self._update_next_run_time()
|
self._update_next_run_time()
|
||||||
print(f"[OK] 重新调度任务: 每天 {', '.join(self.schedule_times)}")
|
print(f"[OK] 重新调度任务: 每天 {', '.join(self.schedule_times)}")
|
||||||
|
|
||||||
@@ -537,9 +542,15 @@ class PortfolioScheduler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# 调度任务(为每个时间点创建任务)
|
# 调度任务(为每个时间点创建任务)
|
||||||
schedule.clear()
|
# 只清除持仓定时分析的任务,不影响智策和监测任务
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'portfolio_analysis' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
print(f"[OK] 清除了 {len(jobs_to_remove)} 个旧的持仓任务")
|
||||||
|
|
||||||
for time_str in self.schedule_times:
|
for time_str in self.schedule_times:
|
||||||
schedule.every().day.at(time_str).do(self._scheduled_job)
|
job = schedule.every().day.at(time_str).do(self._scheduled_job)
|
||||||
|
job.tag('portfolio_analysis')
|
||||||
print(f"[OK] 添加调度任务: 每天 {time_str}")
|
print(f"[OK] 添加调度任务: 每天 {time_str}")
|
||||||
|
|
||||||
self._update_next_run_time()
|
self._update_next_run_time()
|
||||||
@@ -570,7 +581,15 @@ class PortfolioScheduler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
schedule.clear()
|
|
||||||
|
# 只清除持仓定时分析的任务,不影响其他模块(智策、监测)
|
||||||
|
try:
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'portfolio_analysis' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
print(f"[OK] 清除了 {len(jobs_to_remove)} 个持仓任务")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARN] 清除任务时出错: {e}")
|
||||||
|
|
||||||
# 等待线程结束(最多等待2秒)
|
# 等待线程结束(最多等待2秒)
|
||||||
if self.thread and self.thread.is_alive():
|
if self.thread and self.thread.is_alive():
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class SectorStrategyScheduler:
|
|||||||
self.enabled = False
|
self.enabled = False
|
||||||
self.last_run_time = None
|
self.last_run_time = None
|
||||||
self.last_result = None
|
self.last_result = None
|
||||||
|
self.last_notification_time = None # 记录上次通知时间,防止重复
|
||||||
|
self._analysis_lock = threading.Lock() # 添加锁,防止并发执行
|
||||||
print("[智策定时] 调度器初始化完成")
|
print("[智策定时] 调度器初始化完成")
|
||||||
|
|
||||||
def start(self, schedule_time="09:00"):
|
def start(self, schedule_time="09:00"):
|
||||||
@@ -38,14 +40,24 @@ class SectorStrategyScheduler:
|
|||||||
|
|
||||||
self.schedule_time = schedule_time
|
self.schedule_time = schedule_time
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
|
|
||||||
|
# 先清除所有带sector_strategy标签的任务
|
||||||
|
try:
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'sector_strategy' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
print(f"[智策定时] 清除了 {len(jobs_to_remove)} 个旧任务")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[智策定时] 清除旧任务时出错: {e}")
|
||||||
|
|
||||||
|
# 设置定时任务(确保只添加一次)
|
||||||
|
job = schedule.every().day.at(schedule_time).do(self._run_analysis_safe)
|
||||||
|
job.tag('sector_strategy')
|
||||||
|
print(f"[智策定时] 添加新任务: 每天 {schedule_time}")
|
||||||
|
|
||||||
|
# 设置运行标志
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
# 清除之前的任务
|
|
||||||
schedule.clear('sector_strategy')
|
|
||||||
|
|
||||||
# 设置定时任务
|
|
||||||
schedule.every().day.at(schedule_time).do(self._run_analysis).tag('sector_strategy')
|
|
||||||
|
|
||||||
# 启动后台线程
|
# 启动后台线程
|
||||||
self.thread = threading.Thread(target=self._schedule_loop, daemon=True)
|
self.thread = threading.Thread(target=self._schedule_loop, daemon=True)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
@@ -61,7 +73,12 @@ class SectorStrategyScheduler:
|
|||||||
|
|
||||||
self.running = False
|
self.running = False
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
schedule.clear('sector_strategy')
|
|
||||||
|
# 只清除智策的任务,不影响其他模块
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'sector_strategy' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
print(f"[智策定时] 清除了 {len(jobs_to_remove)} 个任务")
|
||||||
|
|
||||||
print("[智策定时] ✓ 定时任务已停止")
|
print("[智策定时] ✓ 定时任务已停止")
|
||||||
return True
|
return True
|
||||||
@@ -78,6 +95,18 @@ class SectorStrategyScheduler:
|
|||||||
print(f"[智策定时] ✗ 调度循环出错: {e}")
|
print(f"[智策定时] ✗ 调度循环出错: {e}")
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
|
||||||
|
def _run_analysis_safe(self):
|
||||||
|
"""运行智策分析(带锁保护,防止并发执行)"""
|
||||||
|
# 尝试获取锁,如果已被占用则跳过本次执行
|
||||||
|
if not self._analysis_lock.acquire(blocking=False):
|
||||||
|
print("[智策定时] ⚠️ 上一次分析还未完成,跳过本次执行")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._run_analysis()
|
||||||
|
finally:
|
||||||
|
self._analysis_lock.release()
|
||||||
|
|
||||||
def _run_analysis(self):
|
def _run_analysis(self):
|
||||||
"""运行智策分析"""
|
"""运行智策分析"""
|
||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
@@ -128,8 +157,16 @@ class SectorStrategyScheduler:
|
|||||||
self._send_error_notification(f"分析异常: {str(e)}")
|
self._send_error_notification(f"分析异常: {str(e)}")
|
||||||
|
|
||||||
def _send_analysis_notification(self, result):
|
def _send_analysis_notification(self, result):
|
||||||
"""发送分析结果通知(邮件和/或webhook)"""
|
"""发送分析结果通知(邮件和/或webhook)- 带去重保护"""
|
||||||
try:
|
try:
|
||||||
|
# 去重检查:如果5分钟内已发送过通知,则跳过
|
||||||
|
current_time = datetime.now()
|
||||||
|
if self.last_notification_time:
|
||||||
|
time_diff = (current_time - self.last_notification_time).total_seconds()
|
||||||
|
if time_diff < 300: # 5分钟 = 300秒
|
||||||
|
print(f"[智策定时] ⚠️ 距离上次通知仅{time_diff:.0f}秒,跳过重复发送")
|
||||||
|
return
|
||||||
|
|
||||||
config = notification_service.config
|
config = notification_service.config
|
||||||
predictions = result.get("final_predictions", {})
|
predictions = result.get("final_predictions", {})
|
||||||
timestamp = result.get("timestamp", datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
timestamp = result.get("timestamp", datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
@@ -163,6 +200,11 @@ class SectorStrategyScheduler:
|
|||||||
else:
|
else:
|
||||||
print("[智策定时] ✗ 邮件发送失败")
|
print("[智策定时] ✗ 邮件发送失败")
|
||||||
|
|
||||||
|
# 更新最后通知时间
|
||||||
|
if sent_count > 0:
|
||||||
|
self.last_notification_time = current_time
|
||||||
|
print(f"[智策定时] 📝 已记录通知时间: {current_time.strftime('%H:%M:%S')}")
|
||||||
|
|
||||||
if sent_count == 0:
|
if sent_count == 0:
|
||||||
print("[智策定时] ⚠️ 未配置通知方式或发送全部失败")
|
print("[智策定时] ⚠️ 未配置通知方式或发送全部失败")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,302 @@
|
|||||||
|
# 智策定时分析通知重复问题 - 修复说明
|
||||||
|
|
||||||
|
## 📋 问题描述
|
||||||
|
|
||||||
|
**症状**:智策定时分析完成后
|
||||||
|
- ✉️ 邮箱收到 **3 封相同邮件**
|
||||||
|
- 📱 钉钉收到 **2 次相同通知**
|
||||||
|
- 所有通知内容完全一样,同时收到
|
||||||
|
|
||||||
|
**定时设置**:
|
||||||
|
- 定时时间: 08:15
|
||||||
|
- 任务状态: 运行中
|
||||||
|
- 上次运行: 2025-10-21 08:21:55
|
||||||
|
|
||||||
|
## 🔍 根本原因分析
|
||||||
|
|
||||||
|
### 原因1:schedule库任务重复添加 ⭐ 主要原因
|
||||||
|
**问题**:`schedule.clear('sector_strategy')` 在某些情况下无法正确清除所有旧任务,导致任务累积。
|
||||||
|
|
||||||
|
**证据**:
|
||||||
|
- 3封邮件表示 `_send_analysis_notification()` 被调用了3次
|
||||||
|
- 说明同一时刻有3个定时任务被触发
|
||||||
|
|
||||||
|
### 原因2:持仓定时分析的干扰
|
||||||
|
**问题**:`portfolio_scheduler.py` 中使用 `schedule.clear()` 会清除**所有**定时任务,包括智策的!
|
||||||
|
|
||||||
|
**代码位置**:
|
||||||
|
- `portfolio_scheduler.py` 第540行:`schedule.clear()`
|
||||||
|
- `portfolio_scheduler.py` 第492行:`schedule.clear()`
|
||||||
|
- `portfolio_scheduler.py` 第573行:`schedule.clear()`
|
||||||
|
|
||||||
|
### 原因3:缺少并发保护
|
||||||
|
**问题**:如果分析还没完成,定时器再次触发,会导致多个分析任务并发执行。
|
||||||
|
|
||||||
|
## ✅ 修复方案
|
||||||
|
|
||||||
|
### 修复1:智策调度器改进(sector_strategy_scheduler.py)
|
||||||
|
|
||||||
|
#### 1.1 添加并发保护锁
|
||||||
|
```python
|
||||||
|
def __init__(self):
|
||||||
|
# ...
|
||||||
|
self._analysis_lock = threading.Lock() # 防止并发执行
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 安全的任务执行包装
|
||||||
|
```python
|
||||||
|
def _run_analysis_safe(self):
|
||||||
|
"""运行智策分析(带锁保护,防止并发执行)"""
|
||||||
|
if not self._analysis_lock.acquire(blocking=False):
|
||||||
|
print("[智策定时] ⚠️ 上一次分析还未完成,跳过本次执行")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._run_analysis()
|
||||||
|
finally:
|
||||||
|
self._analysis_lock.release()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 改进任务清除逻辑
|
||||||
|
**修复前**:
|
||||||
|
```python
|
||||||
|
schedule.clear('sector_strategy') # 可能无法正确清除
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复后**:
|
||||||
|
```python
|
||||||
|
# 手动遍历并清除所有带sector_strategy标签的任务
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'sector_strategy' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
print(f"[智策定时] 清除了 {len(jobs_to_remove)} 个旧任务")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 添加通知去重机制 ⭐ 核心防护
|
||||||
|
```python
|
||||||
|
def _send_analysis_notification(self, result):
|
||||||
|
# 去重检查:如果5分钟内已发送过通知,则跳过
|
||||||
|
current_time = datetime.now()
|
||||||
|
if self.last_notification_time:
|
||||||
|
time_diff = (current_time - self.last_notification_time).total_seconds()
|
||||||
|
if time_diff < 300: # 5分钟 = 300秒
|
||||||
|
print(f"[智策定时] ⚠️ 距离上次通知仅{time_diff:.0f}秒,跳过重复发送")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ... 发送通知 ...
|
||||||
|
|
||||||
|
# 更新最后通知时间
|
||||||
|
if sent_count > 0:
|
||||||
|
self.last_notification_time = current_time
|
||||||
|
print(f"[智策定时] 📝 已记录通知时间: {current_time.strftime('%H:%M:%S')}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复2:持仓调度器改进(portfolio_scheduler.py)
|
||||||
|
|
||||||
|
#### 2.1 使用标签化任务管理
|
||||||
|
**修复前**:
|
||||||
|
```python
|
||||||
|
schedule.clear() # 危险!会清除所有任务
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复后**:
|
||||||
|
```python
|
||||||
|
# 只清除持仓定时分析的任务,不影响智策和监测
|
||||||
|
jobs_to_remove = [job for job in schedule.jobs if 'portfolio_analysis' in job.tags]
|
||||||
|
for job in jobs_to_remove:
|
||||||
|
schedule.cancel_job(job)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 添加任务标签
|
||||||
|
```python
|
||||||
|
for time_str in self.schedule_times:
|
||||||
|
job = schedule.every().day.at(time_str).do(self._scheduled_job)
|
||||||
|
job.tag('portfolio_analysis') # 打标签,方便管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 修复效果
|
||||||
|
|
||||||
|
### 修复前
|
||||||
|
```
|
||||||
|
08:15:00 → 触发任务1 → 发送邮件 ✉️
|
||||||
|
08:15:00 → 触发任务2 → 发送邮件 ✉️ (重复!)
|
||||||
|
08:15:00 → 触发任务3 → 发送邮件 ✉️ (重复!)
|
||||||
|
```
|
||||||
|
**结果**:收到3封邮件
|
||||||
|
|
||||||
|
### 修复后
|
||||||
|
```
|
||||||
|
08:15:00 → 触发任务1 → 获取锁 → 发送邮件 ✉️
|
||||||
|
08:15:00 → 触发任务2 → 无法获取锁 → 跳过
|
||||||
|
08:15:00 → 触发任务3 → 无法获取锁 → 跳过
|
||||||
|
```
|
||||||
|
**结果**:只收到1封邮件
|
||||||
|
|
||||||
|
### 多层防护机制
|
||||||
|
1. **任务去重**:改进的清除逻辑防止任务累积
|
||||||
|
2. **并发锁**:防止多个任务同时执行
|
||||||
|
3. **时间去重**:5分钟内重复通知直接跳过
|
||||||
|
4. **模块隔离**:各模块任务独立管理,互不影响
|
||||||
|
|
||||||
|
## 🧪 测试方法
|
||||||
|
|
||||||
|
### 测试1:验证任务唯一性
|
||||||
|
```python
|
||||||
|
# 在智策定时分析UI中
|
||||||
|
1. 启动定时任务
|
||||||
|
2. 运行以下代码检查任务数量:
|
||||||
|
import schedule
|
||||||
|
sector_jobs = [job for job in schedule.jobs if 'sector_strategy' in job.tags]
|
||||||
|
print(f"智策任务数量: {len(sector_jobs)}") # 应该只有1个
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试2:验证去重机制
|
||||||
|
```python
|
||||||
|
# 手动触发两次分析
|
||||||
|
1. 点击"立即运行"
|
||||||
|
2. 等待1分钟
|
||||||
|
3. 再次点击"立即运行"
|
||||||
|
# 第二次应该显示"距离上次通知仅XX秒,跳过重复发送"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试3:验证模块隔离
|
||||||
|
```python
|
||||||
|
# 同时运行智策和持仓定时分析
|
||||||
|
1. 启动智策定时任务
|
||||||
|
2. 启动持仓定时任务
|
||||||
|
3. 检查两个任务是否都正常运行
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试4:验证实际效果(最重要)
|
||||||
|
```python
|
||||||
|
# 等待下一次定时触发(明天08:15)
|
||||||
|
1. 确认只收到1封邮件
|
||||||
|
2. 确认只收到1次钉钉通知
|
||||||
|
3. 查看日志输出,确认只有1次发送记录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 日志输出示例
|
||||||
|
|
||||||
|
### 正常情况(修复后)
|
||||||
|
```
|
||||||
|
[智策定时] 开始定时分析 - 2025-10-22 08:15:00
|
||||||
|
[智策定时] [1/3] 获取市场数据...
|
||||||
|
[智策定时] ✓ 数据获取成功
|
||||||
|
[智策定时] [2/3] AI智能体分析中...
|
||||||
|
[智策定时] ✓ 分析完成
|
||||||
|
[智策定时] [3/3] 发送邮件通知...
|
||||||
|
[智策定时] [Webhook] 准备发送...
|
||||||
|
[智策定时] ✓ Webhook发送成功
|
||||||
|
[智策定时] [邮件] 准备发送...
|
||||||
|
[智策定时] ✓ 邮件发送成功
|
||||||
|
[智策定时] 📝 已记录通知时间: 08:15:23
|
||||||
|
[智策定时] ✓ 定时分析完成!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重复触发时(修复后)
|
||||||
|
```
|
||||||
|
[智策定时] ⚠️ 上一次分析还未完成,跳过本次执行
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5分钟内重复通知时(修复后)
|
||||||
|
```
|
||||||
|
[智策定时] ⚠️ 距离上次通知仅123秒,跳过重复发送
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 如何使用修复后的版本
|
||||||
|
|
||||||
|
### 步骤1:重启系统
|
||||||
|
```bash
|
||||||
|
# 停止当前运行的streamlit
|
||||||
|
Ctrl+C
|
||||||
|
|
||||||
|
# 重新启动
|
||||||
|
python run.py
|
||||||
|
# 或
|
||||||
|
streamlit run app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:重新配置定时任务
|
||||||
|
1. 进入"智策板块分析"页面
|
||||||
|
2. 找到"定时分析"标签页
|
||||||
|
3. 点击"停止"(如果正在运行)
|
||||||
|
4. 重新设置定时时间为 08:15
|
||||||
|
5. 点击"启动"
|
||||||
|
|
||||||
|
### 步骤3:验证状态
|
||||||
|
查看页面显示:
|
||||||
|
```
|
||||||
|
✅ 定时任务运行中
|
||||||
|
⏰ 定时时间: 08:15
|
||||||
|
📅 下次运行: 2025-10-22 08:15:00
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤4:等待下次执行
|
||||||
|
明天08:15自动执行后,检查:
|
||||||
|
- ✅ 邮箱只收到1封邮件
|
||||||
|
- ✅ 钉钉只收到1次通知
|
||||||
|
- ✅ UI显示正常的执行时间
|
||||||
|
|
||||||
|
## 🛡️ 预防措施
|
||||||
|
|
||||||
|
### 1. 避免重复启动
|
||||||
|
- 在启动新任务前,先检查是否已有任务在运行
|
||||||
|
- 如果要修改时间,先"停止"再重新"启动"
|
||||||
|
|
||||||
|
### 2. 避免频繁手动触发
|
||||||
|
- "立即运行"功能会立即执行分析并发送通知
|
||||||
|
- 避免在5分钟内多次点击
|
||||||
|
|
||||||
|
### 3. 查看日志
|
||||||
|
运行时在控制台查看日志输出,确认:
|
||||||
|
- 任务数量是否正确
|
||||||
|
- 是否有重复执行的警告
|
||||||
|
- 通知发送次数是否正确
|
||||||
|
|
||||||
|
## 📚 技术要点
|
||||||
|
|
||||||
|
### 任务标签体系
|
||||||
|
- `sector_strategy` - 智策定时分析
|
||||||
|
- `portfolio_analysis` - 持仓定时分析
|
||||||
|
- `monitor` - 实时监测定时功能
|
||||||
|
|
||||||
|
### 锁机制
|
||||||
|
使用 `threading.Lock()` 的非阻塞获取:
|
||||||
|
- `acquire(blocking=False)` - 如果锁已被占用,立即返回False
|
||||||
|
- 保证同一时刻只有一个分析任务在执行
|
||||||
|
|
||||||
|
### 时间去重
|
||||||
|
- 记录 `last_notification_time`
|
||||||
|
- 每次发送前检查时间差
|
||||||
|
- 5分钟(300秒)内的重复请求直接跳过
|
||||||
|
|
||||||
|
## ✅ 修复清单
|
||||||
|
|
||||||
|
- [x] 智策调度器添加并发保护锁
|
||||||
|
- [x] 智策调度器添加通知去重机制(5分钟)
|
||||||
|
- [x] 智策调度器改进任务清除逻辑
|
||||||
|
- [x] 持仓调度器使用标签化任务管理
|
||||||
|
- [x] 持仓调度器避免清除其他模块任务
|
||||||
|
- [x] 添加详细日志输出
|
||||||
|
- [x] 创建测试方案
|
||||||
|
- [x] 编写修复说明文档
|
||||||
|
|
||||||
|
## 🎯 预期效果
|
||||||
|
|
||||||
|
修复后,智策定时分析应该:
|
||||||
|
1. ✅ 每次只发送 **1封** 邮件
|
||||||
|
2. ✅ 每次只发送 **1次** 钉钉通知
|
||||||
|
3. ✅ 不会受到持仓定时分析的影响
|
||||||
|
4. ✅ 即使误操作多次启动,也有去重保护
|
||||||
|
5. ✅ 日志清晰,便于排查问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修复日期**:2025-10-21
|
||||||
|
**修复版本**:v1.1.0
|
||||||
|
**修复文件**:
|
||||||
|
- `sector_strategy_scheduler.py` ✅
|
||||||
|
- `portfolio_scheduler.py` ✅
|
||||||
|
|
||||||
|
**下次执行验证**:2025-10-22 08:15:00
|
||||||
|
|
||||||
Reference in New Issue
Block a user