diff --git a/.gitignore b/.gitignore
index 1dfa6a0..c4ce7ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,15 @@
.env
/.cursor
/openspec
-TradEnv/
\ No newline at end of file
+TradEnv/
+<<<<<<< HEAD
+*.db
+=======
+stock_analysis.db
+sector_strategy.db
+longhubang.db
+main_force_batch.db
+portfolio_stocks.db
+smart_monitor.db
+stock_monitor.db
+>>>>>>> 3554818 (更新界面布局“)
diff --git a/app.py b/app.py
index f030097..c04f55e 100644
--- a/app.py
+++ b/app.py
@@ -273,6 +273,57 @@ def model_selector():
padding: 0 1rem;
}
}
+
+ /* 优化记录卡片布局 */
+ .record-card {
+ margin-bottom: 0.5rem !important;
+ padding: 0.8rem !important;
+ border-radius: 8px !important;
+ border: 1px solid #e0e0e0 !important;
+ background: #fafafa !important;
+ }
+
+ /* 紧凑的H4标题样式 */
+ .compact-h4 {
+ margin-bottom: 0.05rem !important;
+ line-height: 1.3 !important;
+ font-size: 1.5rem !important;
+ padding: 0 !important;
+ margin-top: 0 !important;
+ }
+
+ /* 紧凑的P元素样式 */
+ .compact-p {
+ margin-top: 0.05rem !important;
+ margin-bottom: 0 !important;
+ line-height: 1.3 !important;
+ font-size: 1.2rem !important;
+ padding: 0 !important;
+ }
+
+ /* 优化记录卡片内部间距 */
+ .record-card .stMarkdown {
+ margin-bottom: 0.2rem !important;
+ }
+
+ /* 确保P元素与DIV容器对齐 */
+ .record-card .stMarkdown p {
+ margin: 0.05rem 0 !important;
+ padding: 0 !important;
+ }
+
+ /* 按钮区域优化 */
+ .button-row {
+ display: flex !important;
+ gap: 0.3rem !important;
+ align-items: center !important;
+ }
+
+ /* 确保文字区域不超过页面1/3 */
+ .text-content-area {
+ max-width: 33% !important;
+ flex: 1 !important;
+ }
""", unsafe_allow_html=True)
@@ -292,10 +343,9 @@ def main():
# 🏠 单股分析(首页)
if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"):
- # 清除所有功能页面标志
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
- 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
- if key in st.session_state:
+ # 清除所有功能页面标志 - 优化:使用st.session_state.clear()快速清除
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
st.markdown("---")
@@ -305,93 +355,93 @@ def main():
st.markdown("**根据不同策略筛选优质股票**")
if st.button("💰 主力选股", width='stretch', key="nav_main_force", help="基于主力资金流向的选股策略"):
- st.session_state.show_main_force = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
- 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_main_force = True
if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"):
- st.session_state.show_low_price_bull = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
- 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_small_cap', 'show_profit_growth']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_low_price_bull = True
if st.button("📊 小市值策略", width='stretch', key="nav_small_cap", help="小盘高成长股票筛选策略"):
- st.session_state.show_small_cap = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
- 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_profit_growth']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_small_cap = True
if st.button("📈 净利增长", width='stretch', key="nav_profit_growth", help="净利润增长稳健股票筛选策略"):
- st.session_state.show_profit_growth = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
- 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_small_cap']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_profit_growth = True
# 📊 策略分析
with st.expander("📊 策略分析", expanded=True):
st.markdown("**AI驱动的板块和龙虎榜策略**")
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"):
- st.session_state.show_sector_strategy = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
- 'show_longhubang', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_sector_strategy = True
if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"):
- st.session_state.show_longhubang = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
- 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_longhubang = True
# 💼 投资管理
with st.expander("💼 投资管理", expanded=True):
st.markdown("**持仓跟踪与实时监测**")
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"):
- st.session_state.show_portfolio = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
- 'show_sector_strategy', 'show_longhubang', 'show_smart_monitor', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_portfolio = True
if st.button("🤖 AI盯盘", width='stretch', key="nav_smart_monitor", help="DeepSeek AI自动盯盘决策交易(支持A股T+1)"):
- st.session_state.show_smart_monitor = True
- for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
- 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_smart_monitor = True
if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"):
- st.session_state.show_monitor = True
- for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio',
- 'show_config', 'show_sector_strategy', 'show_smart_monitor', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_monitor = True
st.markdown("---")
# 📖 历史记录
if st.button("📖 历史记录", width='stretch', key="nav_history", help="查看历史分析记录"):
- st.session_state.show_history = True
- for key in ['show_monitor', 'show_longhubang', 'show_portfolio', 'show_config',
- 'show_main_force', 'show_sector_strategy', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_history = True
# ⚙️ 环境配置
if st.button("⚙️ 环境配置", width='stretch', key="nav_config", help="系统设置与API配置"):
- st.session_state.show_config = True
- for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy',
- 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
- if key in st.session_state:
+ # 优化:先清除所有show_前缀的状态,再设置当前状态
+ for key in list(st.session_state.keys()):
+ if key.startswith('show_'):
del st.session_state[key]
+ st.session_state.show_config = True
st.markdown("---")
@@ -561,10 +611,10 @@ def main():
)
with col2:
- analyze_button = st.button("🚀 开始分析", type="primary", width='stretch')
+ analyze_button = st.button("🚀 开始分析", type="primary", width='stretch', key="analyze_single_stock")
with col3:
- if st.button("🔄 清除缓存", width='stretch'):
+ if st.button("🔄 清除缓存", width='stretch', key="clear_cache_single"):
st.cache_data.clear()
st.success("缓存已清除")
@@ -579,13 +629,13 @@ def main():
col1, col2, col3 = st.columns(3)
with col1:
- analyze_button = st.button("🚀 开始批量分析", type="primary", width='stretch')
+ analyze_button = st.button("🚀 开始批量分析", type="primary", width='stretch', key="analyze_batch")
with col2:
- if st.button("🔄 清除缓存", width='stretch'):
+ if st.button("🔄 清除缓存", width='stretch', key="clear_cache_batch"):
st.cache_data.clear()
st.success("缓存已清除")
with col3:
- if st.button("🗑️ 清除结果", width='stretch'):
+ if st.button("🗑️ 清除结果", width='stretch', key="clear_results"):
if 'batch_analysis_results' in st.session_state:
del st.session_state.batch_analysis_results
st.success("已清除批量分析结果")
@@ -1200,7 +1250,7 @@ def run_stock_analysis(symbol, period):
news_data = news_fetcher.get_stock_news(symbol)
if news_data and news_data.get('data_success'):
news_count = news_data.get('news_data', {}).get('count', 0) if news_data.get('news_data') else 0
- st.info(f"✅ 成功从东方财富获取个股 {news_count} 条新闻")
+ st.info(f"✅ 成功获取个股 {news_count} 条新闻")
else:
st.warning("⚠️ 未能获取新闻数据,将基于基本信息进行分析")
except Exception as e:
@@ -1661,6 +1711,28 @@ def display_history_records():
"""显示历史分析记录"""
st.subheader("📚 历史分析记录")
+ # 滚动位置管理 - 确保展开详情时页面不会滚动到错误位置
+ if 'scroll_to_record_id' in st.session_state:
+ scroll_target_id = st.session_state.scroll_to_record_id
+ # 添加滚动JavaScript
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+ # 清理滚动状态
+ del st.session_state.scroll_to_record_id
+
# 获取所有记录
records = db.get_all_records()
@@ -1677,7 +1749,7 @@ def display_history_records():
with col2:
st.write("")
st.write("")
- if st.button("🔄 刷新列表"):
+ if st.button("🔄 刷新列表", key="refresh_history_list"):
st.rerun()
# 筛选记录
@@ -1705,40 +1777,84 @@ def display_history_records():
"强烈卖出": "🔴"
}.get(rating, "⚪")
- with st.expander(f"{rating_color} {record['stock_name']} ({record['symbol']}) - {record['analysis_date']}"):
- col1, col2, col3, col4 = st.columns([2, 2, 1, 1])
-
- with col1:
- st.write(f"**股票代码:** {record['symbol']}")
- st.write(f"**股票名称:** {record['stock_name']}")
-
- with col2:
- st.write(f"**分析时间:** {record['analysis_date']}")
- st.write(f"**数据周期:** {record['period']}")
- st.write(f"**投资评级:** **{rating}**")
-
- with col3:
- if st.button("👀 查看详情", key=f"view_{record['id']}"):
- st.session_state.viewing_record_id = record['id']
-
- with col4:
- if st.button("➕ 监测", key=f"add_monitor_{record['id']}"):
+ # 为记录添加锚点标记和卡片容器
+ st.markdown(f'
', unsafe_allow_html=True)
+
+ # 创建记录展示 - 优化布局
+ # 使用2列主布局:左侧文本区域 + 右侧按钮区域
+ text_col, button_col = st.columns([3, 2], gap="small")
+
+ with text_col:
+ # 添加flex容器确保与按钮区域高度一致
+ st.markdown("", unsafe_allow_html=True)
+
+ # 第一行:股票代码和名称 + 投资评级(使用相同比例的嵌套列)
+ top_row_col1, top_row_col2 = st.columns([2, 1])
+
+ with top_row_col1:
+ st.markdown(f"
{record['symbol']} - {record['stock_name']}
", unsafe_allow_html=True)
+
+ with top_row_col2:
+ st.markdown(f"
投资评级: {rating}
", unsafe_allow_html=True)
+
+ # 第二行:分析时间 + 数据周期(与第一行使用相同比例的嵌套列)
+ bottom_row_col1, bottom_row_col2 = st.columns([2, 1])
+
+ with bottom_row_col1:
+ st.markdown(f"
分析时间: {record['analysis_date']}
", unsafe_allow_html=True)
+
+ with bottom_row_col2:
+ st.markdown(f"
数据周期: {record['period']}
", unsafe_allow_html=True)
+
+ # 闭合flex容器
+ st.markdown("
", unsafe_allow_html=True)
+
+ with button_col:
+ # 按钮区域:使用flex布局确保垂直对齐
+ st.markdown("", unsafe_allow_html=True)
+
+ # 按钮行:查看详情、监测、删除(紧凑布局)
+ btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 1])
+
+ with btn_col1:
+ # 获取当前记录的详情显示状态
+ show_detail = st.session_state.get(f"show_detail_{record['id']}", False)
+ button_text = "👁️ 收起" if show_detail else "👀 详情"
+ if st.button(button_text, key=f"view_{record['id']}", use_container_width=True):
+ st.session_state[f"show_detail_{record['id']}"] = not show_detail
+
+ # 如果是收起详情操作,设置滚动目标
+ if show_detail: # 当前是展开状态,点击后要收起
+ st.session_state.scroll_to_record_id = record['id']
+
+ st.rerun()
+
+ with btn_col2:
+ if st.button("➕ 监测", key=f"add_monitor_{record['id']}", use_container_width=True):
st.session_state.add_to_monitor_id = record['id']
- st.session_state.viewing_record_id = record['id']
-
- # 删除按钮(新增一行)
- col5, _, _, _ = st.columns(4)
- with col5:
- if st.button("🗑️ 删除", key=f"delete_{record['id']}"):
+ st.session_state[f"show_detail_{record['id']}"] = True
+ # 设置滚动目标,确保展开详情后页面位置正确
+ st.session_state.scroll_to_record_id = record['id']
+ st.rerun()
+
+ with btn_col3:
+ if st.button("🗑️ 删除", key=f"delete_{record['id']}", use_container_width=True):
if db.delete_record(record['id']):
st.success("✅ 记录已删除")
st.rerun()
else:
st.error("❌ 删除失败")
+
+ # 闭合flex容器
+ st.markdown("
", unsafe_allow_html=True)
- # 查看详细记录
- if 'viewing_record_id' in st.session_state:
- display_record_detail(st.session_state.viewing_record_id)
+ # 显示详情(在记录框架内展开)
+ if show_detail:
+ st.markdown("---")
+ st.markdown("#### 📊 详细分析记录")
+ display_record_detail(record['id'])
+
+ st.markdown("---")
def display_add_to_monitor_dialog(record):
"""显示加入监测的对话框"""
@@ -1911,7 +2027,7 @@ def display_add_to_monitor_dialog(record):
st.rerun()
else:
st.warning("⚠️ 无法从分析结果中提取关键数据")
- if st.button("❌ 取消"):
+ if st.button("❌ 取消", key="cancel_add_monitor"):
if 'add_to_monitor_id' in st.session_state:
del st.session_state.add_to_monitor_id
st.rerun()
@@ -1921,6 +2037,8 @@ def display_record_detail(record_id):
st.markdown("---")
st.subheader("📋 详细分析记录")
+ # 移除顶部的分隔线和标题,因为已经在调用处添加
+
record = db.get_record_by_id(record_id)
if not record:
st.error("❌ 记录不存在")
@@ -2053,7 +2171,7 @@ def display_record_detail(record_id):
decision_text = final_decision.get('decision_text', str(final_decision))
st.write(decision_text)
- # 加入监测功能
+ # 操作功能区域
st.markdown("---")
st.subheader("🎯 操作")
@@ -2061,22 +2179,31 @@ def display_record_detail(record_id):
if 'add_to_monitor_id' in st.session_state and st.session_state.add_to_monitor_id == record_id:
display_add_to_monitor_dialog(record)
else:
- # 只有在不显示对话框时才显示按钮
- col1, col2 = st.columns([1, 3])
-
- with col1:
- if st.button("➕ 加入监测", type="primary", width='stretch'):
+ # 将加入监测和收起详情按钮放在同一行并靠右对齐
+ col1, col2, col3 = st.columns([2, 1, 1])
+
+ with col2:
+ if st.button("➕ 加入监测", type="primary", width='stretch', key=f"add_monitor_btn_{record_id}"):
st.session_state.add_to_monitor_id = record_id
st.rerun()
-
- # 返回按钮
- st.markdown("---")
- if st.button("⬅️ 返回历史记录列表"):
- if 'viewing_record_id' in st.session_state:
- del st.session_state.viewing_record_id
- if 'add_to_monitor_id' in st.session_state:
- del st.session_state.add_to_monitor_id
- st.rerun()
+
+ with col3:
+ if st.button("👁️ 收起详情", key=f"close_detail_btn_{record_id}"):
+ # 设置滚动目标,确保页面回到正确的记录位置
+ st.session_state.scroll_to_record_id = record_id
+
+ # 清理详情页面状态
+ if 'viewing_record_id' in st.session_state:
+ del st.session_state.viewing_record_id
+ if 'add_to_monitor_id' in st.session_state:
+ del st.session_state.add_to_monitor_id
+
+ # 清理历史记录列表中的详情显示状态
+ st.session_state[f"show_detail_{record_id}"] = False
+
+ # 使用 st.rerun() 重新渲染页面
+ st.rerun()
+
def display_config_manager():
"""显示环境配置管理界面"""
@@ -2430,7 +2557,7 @@ def display_config_manager():
col1, col2, col3, col4 = st.columns([1, 1, 1, 2])
with col1:
- if st.button("💾 保存配置", type="primary", width='stretch'):
+ if st.button("💾 保存配置", type="primary", width='stretch', key="save_config_btn"):
# 验证配置
is_valid, message = config_manager.validate_config(st.session_state.temp_config)
@@ -2455,14 +2582,14 @@ def display_config_manager():
st.error(f"❌ 配置验证失败: {message}")
with col2:
- if st.button("🔄 重置", width='stretch'):
+ if st.button("🔄 重置", width='stretch', key="reset_config_btn"):
# 重置为当前文件中的值
st.session_state.temp_config = {key: info["value"] for key, info in config_info.items()}
st.success("✅ 已重置为当前配置")
st.rerun()
with col3:
- if st.button("⬅️ 返回", width='stretch'):
+ if st.button("⬅️ 返回", width='stretch', key="back_from_config_btn"):
if 'show_config' in st.session_state:
del st.session_state.show_config
if 'temp_config' in st.session_state:
diff --git a/config.py b/config.py
index 5d3830b..7912662 100644
--- a/config.py
+++ b/config.py
@@ -19,6 +19,7 @@
MINIQMT_CONFIG = {
'enabled': os.getenv("MINIQMT_ENABLED", "false").lower() == "true",
'account_id': os.getenv("MINIQMT_ACCOUNT_ID", ""),
+ 'path': os.getenv("MINIQMT_PATH", "D:\\qmt\\userdata_mini"),
'host': os.getenv("MINIQMT_HOST", "127.0.0.1"),
'port': int(os.getenv("MINIQMT_PORT", "58610")),
}
diff --git a/config_manager.py b/config_manager.py
index 3881b50..63aed5e 100644
--- a/config_manager.py
+++ b/config_manager.py
@@ -56,6 +56,12 @@ def __init__(self, env_file: str = ".env"):
"required": False,
"type": "text"
},
+ "MINIQMT_PATH": {
+ "value": "",
+ "description": "MiniQMT安装路径(例如:C:\\国金证券QMT\\userdata_mini)",
+ "required": False,
+ "type": "text"
+ },
"EMAIL_ENABLED": {
"value": "false",
"description": "启用邮件通知",
@@ -185,6 +191,7 @@ def write_env(self, config: Dict[str, str]) -> bool:
lines.append(f'MINIQMT_ACCOUNT_ID="{config.get("MINIQMT_ACCOUNT_ID", "")}"')
lines.append(f'MINIQMT_HOST="{config.get("MINIQMT_HOST", "127.0.0.1")}"')
lines.append(f'MINIQMT_PORT="{config.get("MINIQMT_PORT", "58610")}"')
+ lines.append(f'MINIQMT_PATH="{config.get("MINIQMT_PATH", "")}"')
lines.append("")
# 邮件通知配置
diff --git a/docker-compose.yml b/docker-compose.yml
index 786b75e..306aeaa 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
agentsstock:
build:
diff --git "a/docs/toshare\350\257\264\346\230\216.md" "b/docs/tushare\350\257\264\346\230\216.md"
similarity index 98%
rename from "docs/toshare\350\257\264\346\230\216.md"
rename to "docs/tushare\350\257\264\346\230\216.md"
index a46cb13..19cdb2d 100644
--- "a/docs/toshare\350\257\264\346\230\216.md"
+++ "b/docs/tushare\350\257\264\346\230\216.md"
@@ -44,7 +44,8 @@ df = pro.query('trade_cal', exchange='', start_date='20180901', end_date='201810
沪深港通资金流向
-接口:moneyflow_hsgt,可以通过数据工具调试和查看数据。
+接口:moneyflow_hsgt
+可以通过数据工具调试和查看数据。
描述:获取沪股通、深股通、港股通每日资金流向数据,每次最多返回300条记录,总量不限制。每天18~20点之间完成当日更新
积分要求:2000积分起,5000积分每分钟可提取500次
diff --git "a/docs/\345\214\227\345\220\221\346\225\260\346\215\256\347\244\272\344\276\213.xlsx" "b/docs/\345\214\227\345\220\221\346\225\260\346\215\256\347\244\272\344\276\213.xlsx"
new file mode 100644
index 0000000..c83e0bf
Binary files /dev/null and "b/docs/\345\214\227\345\220\221\346\225\260\346\215\256\347\244\272\344\276\213.xlsx" differ
diff --git a/longhubang.db b/longhubang.db
index f1c273b..825a4e3 100644
Binary files a/longhubang.db and b/longhubang.db differ
diff --git a/longhubang_data.py b/longhubang_data.py
index 501d5c3..f90c3af 100644
--- a/longhubang_data.py
+++ b/longhubang_data.py
@@ -24,8 +24,8 @@ def __init__(self, api_key=None):
"""
print("[智瞰龙虎] 龙虎榜数据获取器初始化...")
# self.base_url = "https://api-lhb.zhongdu.net"
- self.base_url = "http://lhb-api.ws4.cn/v1"
- # self.base_url = "https://www.stockapi.com.cn/v1"
+ # self.base_url = "http://lhb-api.ws4.cn/v1"
+ self.base_url = "https://www.stockapi.com.cn/v1"
self.api_key = api_key
self.max_retries = 3 # 最大重试次数
self.retry_delay = 2 # 重试延迟(秒)
@@ -81,7 +81,7 @@ def get_longhubang_data(self, date):
"""
print(f"[智瞰龙虎] 获取 {date} 的龙虎榜数据...")
- # url = f"{self.base_url}"
+ # 使用兼容接口获取指定日期数据
url = f"{self.base_url}/youzi/all"
params = {'date': date}
@@ -128,12 +128,12 @@ def get_longhubang_data_range(self, start_date, end_date):
print(f"[智瞰龙虎] ✓ 共获取 {len(all_data)} 条记录")
return all_data
- def get_recent_days_data(self, days=5):
+ def get_recent_days_data(self, days=3):
"""
获取最近N个交易日的龙虎榜数据
Args:
- days: 天数(默认5天)
+ days: 天数(默认3天)
Returns:
list: 龙虎榜数据列表
diff --git a/longhubang_ui.py b/longhubang_ui.py
index e10dc60..aaa9c98 100644
--- a/longhubang_ui.py
+++ b/longhubang_ui.py
@@ -131,7 +131,7 @@ def display_analysis_tab():
"最近天数",
min_value=1,
max_value=10,
- value=1,
+ value=3,
help="分析最近N天的龙虎榜数据"
)
@@ -461,7 +461,7 @@ def display_scoring_ranking(result):
showlegend=False,
height=400
)
- st.plotly_chart(fig1, config={'displayModeBar': False}, use_container_width=True)
+ st.plotly_chart(fig1, config={'displayModeBar': False}, width='stretch')
with col2:
# 五维评分雷达图(显示批量分析数量的股票)
@@ -509,7 +509,7 @@ def display_scoring_ranking(result):
x=0.5
)
)
- st.plotly_chart(fig2, config={'displayModeBar': False}, use_container_width=True)
+ st.plotly_chart(fig2, config={'displayModeBar': False}, width='stretch')
st.markdown("---")
@@ -703,7 +703,7 @@ def display_visualizations(result):
labels={'name': '股票名称', 'net_inflow': '净流入金额(元)'}
)
fig.update_layout(xaxis_tickangle=-45)
- st.plotly_chart(fig, config={'displayModeBar': False}, use_container_width=True)
+ st.plotly_chart(fig, config={'displayModeBar': False}, width='stretch')
# 热门概念图表
if summary.get('hot_concepts'):
@@ -718,7 +718,7 @@ def display_visualizations(result):
names='概念',
title='热门概念出现次数分布'
)
- st.plotly_chart(fig, config={'displayModeBar': False}, use_container_width=True)
+ st.plotly_chart(fig, config={'displayModeBar': False}, width='stretch')
def display_pdf_export_section(result):
diff --git a/low_price_bull_monitor.db b/low_price_bull_monitor.db
index 1f5cbc1..dad86d6 100644
Binary files a/low_price_bull_monitor.db and b/low_price_bull_monitor.db differ
diff --git a/main_force_batch.db b/main_force_batch.db
index 8e5b6a2..ff094d3 100644
Binary files a/main_force_batch.db and b/main_force_batch.db differ
diff --git a/main_force_ui.py b/main_force_ui.py
index 5477f48..ae39030 100644
--- a/main_force_ui.py
+++ b/main_force_ui.py
@@ -140,8 +140,7 @@ def display_main_force_selector():
st.markdown("---")
# 开始分析按钮
- if st.button("🚀 开始主力选股", type="primary", width='content'):
-
+ if st.button("🚀 开始主力选股", type="primary", width='stretch'):
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
# 创建分析器
diff --git a/miniqmt_interface.py b/miniqmt_interface.py
index d6831bc..eb7192f 100644
--- a/miniqmt_interface.py
+++ b/miniqmt_interface.py
@@ -13,22 +13,22 @@
class TradeAction(Enum):
"""交易动作枚举"""
- BUY = "buy" # 买入
- SELL = "sell" # 卖出
- HOLD = "hold" # 持有
+ BUY = "买入"
+ SELL = "卖出"
+ HOLD = "持有"
class OrderType(Enum):
"""订单类型枚举"""
- MARKET = "market" # 市价单
- LIMIT = "limit" # 限价单
- STOP = "stop" # 止损单
- STOP_LIMIT = "stop_limit" # 止损限价单
+ MARKET = "市价单"
+ LIMIT = "限价单"
+ STOP = "止损单"
+ STOP_LIMIT = "止损限价单"
class PositionSide(Enum):
"""持仓方向枚举"""
- LONG = "long" # 多头
- SHORT = "short" # 空头
- NONE = "none" # 无持仓
+ LONG = "多头"
+ SHORT = "空头"
+ NONE = "无持仓"
class MiniQMTInterface:
"""
@@ -557,7 +557,36 @@ def from_dict(cls, data: Dict):
# 全局MiniQMT接口实例
-miniqmt = MiniQMTInterface()
+miniqmt = None
+
+# 应用启动时初始化MiniQMT
+config = None
+
+try:
+ # 优先从config模块导入配置
+ from config import MINIQMT_CONFIG
+ config = MINIQMT_CONFIG
+except ImportError:
+ # 如果无法导入config模块,则从环境变量读取配置
+ import os
+ config = {
+ 'enabled': os.getenv("MINIQMT_ENABLED", "false").lower() == "true",
+ 'account_id': os.getenv("MINIQMT_ACCOUNT_ID", ""),
+ 'path': os.getenv("MINIQMT_PATH", "D:\\qmt\\userdata_mini"),
+ 'host': os.getenv("MINIQMT_HOST", "127.0.0.1"),
+ 'port': int(os.getenv("MINIQMT_PORT", "58610")),
+ }
+
+# 初始化MiniQMT实例
+miniqmt = MiniQMTInterface(config)
+
+# 如果启用,尝试连接
+if config.get('enabled', False):
+ success, msg = miniqmt.connect()
+ if success:
+ print(f"✅ MiniQMT已连接: {msg}")
+ else:
+ print(f"⚠️ MiniQMT连接失败: {msg}")
def init_miniqmt(config: Dict = None) -> Tuple[bool, str]:
@@ -579,9 +608,13 @@ def init_miniqmt(config: Dict = None) -> Tuple[bool, str]:
from config import MINIQMT_CONFIG
config = MINIQMT_CONFIG
except ImportError:
+ import os
config = {
- 'enabled': False,
- 'account_id': None
+ 'enabled': os.getenv("MINIQMT_ENABLED", "false").lower() == "true",
+ 'account_id': os.getenv("MINIQMT_ACCOUNT_ID", ""),
+ 'path': os.getenv("MINIQMT_PATH", "D:\\qmt\\userdata_mini"),
+ 'host': os.getenv("MINIQMT_HOST", "127.0.0.1"),
+ 'port': int(os.getenv("MINIQMT_PORT", "58610")),
}
miniqmt = MiniQMTInterface(config)
diff --git a/monitor_manager.py b/monitor_manager.py
index 324feb4..f0f562e 100644
--- a/monitor_manager.py
+++ b/monitor_manager.py
@@ -399,7 +399,7 @@ def display_edit_dialog(stock_id: int):
quant_enabled = st.checkbox("启用量化自动交易", value=stock.get('quant_enabled', False))
if quant_enabled:
- quant_config = stock.get('quant_config', {})
+ quant_config = stock.get('quant_config') or {}
max_position_pct = st.slider("最大仓位比例", 0.05, 0.5,
quant_config.get('max_position_pct', 0.2), 0.05)
auto_stop_loss = st.checkbox("自动止损", value=quant_config.get('auto_stop_loss', True))
diff --git a/monitor_service.py b/monitor_service.py
index 5563c75..233d851 100644
--- a/monitor_service.py
+++ b/monitor_service.py
@@ -193,7 +193,7 @@ def _check_trigger_conditions(self, stock: Dict, current_price: float):
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 进入进场区间 [{entry_range['min']}-{entry_range['max']}]"
monitor_db.add_notification(stock['id'], 'entry', message)
- # 立即发送通知(包括邮件)
+ # 立即发送通知(包括邮件和Webhook)
notification_service.send_notifications()
# 如果启用量化交易,执行自动交易
@@ -207,7 +207,7 @@ def _check_trigger_conditions(self, stock: Dict, current_price: float):
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止盈位 {take_profit}"
monitor_db.add_notification(stock['id'], 'take_profit', message)
- # 立即发送通知(包括邮件)
+ # 立即发送通知(包括邮件和Webhook)
notification_service.send_notifications()
# 如果启用量化交易,执行自动交易
@@ -221,7 +221,7 @@ def _check_trigger_conditions(self, stock: Dict, current_price: float):
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止损位 {stop_loss}"
monitor_db.add_notification(stock['id'], 'stop_loss', message)
- # 立即发送通知(包括邮件)
+ # 立即发送通知(包括邮件和Webhook)
notification_service.send_notifications()
# 如果启用量化交易,执行自动交易
@@ -237,7 +237,7 @@ def _execute_quant_trade(self, stock: Dict, signal_type: str, current_price: flo
return
# 获取量化配置
- quant_config = stock.get('quant_config', {})
+ quant_config = stock.get('quant_config') or {}
if not quant_config:
print(f"股票 {stock['symbol']} 未配置量化参数")
return
diff --git a/monitor_ui.py b/monitor_ui.py
index f13c968..ccb7cd6 100644
--- a/monitor_ui.py
+++ b/monitor_ui.py
@@ -12,7 +12,7 @@ def display_monitor_panel():
st.markdown("## 📊 实时监测面板")
# 监测服务控制
- col1, col2, col3, col4 = st.columns([1, 1, 1, 1])
+ col1, col2, col3 = st.columns([1, 1, 1])
with col1:
if st.button("▶️ 启动监测服务", type="primary"):
@@ -29,6 +29,9 @@ def display_monitor_panel():
monitor_service.manual_update_stock(stock['id'])
st.success(f"✅ 已手动更新 {len(stocks)} 只股票")
+ # 显示定时调度状态和通知配置
+ col4, col5 = st.columns([1, 2])
+
with col4:
# 显示定时调度状态
try:
@@ -41,6 +44,22 @@ def display_monitor_panel():
except:
st.info("⏰ 定时未配置")
+ with col5:
+ # 显示通知配置状态
+ email_status = notification_service.get_email_config_status()
+ webhook_status = notification_service.get_webhook_config_status()
+
+ status_text = []
+ if email_status['enabled'] and email_status['configured']:
+ status_text.append("📧 邮件通知已启用")
+ if webhook_status['enabled'] and webhook_status['configured']:
+ status_text.append(f"🔗 Webhook通知已启用 ({webhook_status['webhook_type']})")
+
+ if status_text:
+ st.success(" ".join(status_text))
+ else:
+ st.info("通知服务未完全配置")
+
# 显示通知
display_notifications()
diff --git a/notification_service.py b/notification_service.py
index dbc62d2..826993a 100644
--- a/notification_service.py
+++ b/notification_service.py
@@ -54,6 +54,11 @@ def _load_config(self) -> Dict:
if os.getenv('WEBHOOK_KEYWORD'):
config['webhook_keyword'] = os.getenv('WEBHOOK_KEYWORD')
+ # 自动启用逻辑:如果配置了webhook_url,自动启用webhook通知
+ if config['webhook_url'] and not os.getenv('WEBHOOK_ENABLED'):
+ config['webhook_enabled'] = True
+ print("[自动启用] Webhook通知已根据WEBHOOK_URL配置自动启用")
+
return config
def send_notifications(self):
diff --git a/pdf_generator_pandoc.py b/pdf_generator_pandoc.py
index cdfafec..cf22861 100644
--- a/pdf_generator_pandoc.py
+++ b/pdf_generator_pandoc.py
@@ -277,7 +277,6 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
success = generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
if success:
st.balloons()
-
# 生成Markdown报告按钮
if st.button("📝 生成并下载Markdown报告", type="secondary", width='content', key=markdown_button_key):
with st.spinner("正在生成Markdown报告..."):
@@ -308,7 +307,7 @@ def display_pdf_export_section(stock_info, agents_results, discussion_result, fi
except Exception as e:
st.error(f"❌ 生成Markdown报告时出错: {str(e)}")
-
+
# 如果已经生成了报告,显示下载链接
if st.session_state.show_download_links:
generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
\ No newline at end of file
diff --git a/portfolio_stocks.db b/portfolio_stocks.db
index 9cd4be4..3fbb17c 100644
Binary files a/portfolio_stocks.db and b/portfolio_stocks.db differ
diff --git a/qstock_news_data.py b/qstock_news_data.py
index 1f78a2f..dc73280 100644
--- a/qstock_news_data.py
+++ b/qstock_news_data.py
@@ -95,17 +95,17 @@ def _get_news_data(self, symbol):
news_items = []
- # 方法1: 尝试获取个股新闻(东方财富)
+ # 方法1: 尝试获取CCTV新闻(作为基础新闻源)
try:
- # stock_news_em(symbol="600519") - 东方财富个股新闻
- df = ak.stock_news_em(symbol=symbol)
+ # news_cctv() - CCTV新闻
+ df = ak.news_cctv()
if df is not None and not df.empty:
- print(f" ✓ 从东方财富获取到 {len(df)} 条新闻")
+ print(f" ✓ 从CCTV获取到 {len(df)} 条新闻")
# 处理DataFrame,提取新闻
for idx, row in df.head(self.max_items).iterrows():
- item = {'source': '东方财富'}
+ item = {'source': 'CCTV'}
# 提取所有列
for col in df.columns:
@@ -125,83 +125,51 @@ def _get_news_data(self, symbol):
news_items.append(item)
except Exception as e:
- print(f" ⚠ 从东方财富获取失败: {e}")
+ print(f" ⚠ 从CCTV获取失败: {e}")
- # 方法2: 如果没有获取到,尝试获取新浪财经新闻
- if not news_items:
- try:
- # stock_zh_a_spot_em() - 获取股票信息,包含代码和名称
- df_info = ak.stock_zh_a_spot_em()
-
- # 查找股票名称
- stock_name = None
- if df_info is not None and not df_info.empty:
- match = df_info[df_info['代码'] == symbol]
- if not match.empty:
- stock_name = match.iloc[0]['名称']
- print(f" 找到股票名称: {stock_name}")
-
- # 使用股票名称搜索新闻
- if stock_name:
- # stock_news_sina - 新浪财经新闻
- try:
- df = ak.stock_news_sina(symbol=stock_name)
- if df is not None and not df.empty:
- print(f" ✓ 从新浪财经获取到 {len(df)} 条新闻")
-
- for idx, row in df.head(self.max_items).iterrows():
- item = {'source': '新浪财经'}
-
- for col in df.columns:
- value = row.get(col)
- if value is None or (isinstance(value, float) and pd.isna(value)):
- continue
- try:
- item[col] = str(value)
- except:
- item[col] = "无法解析"
-
- if len(item) > 1:
- news_items.append(item)
- except:
- pass
+ # 方法2: 尝试获取个股相关信息(通过筛选CCTV新闻中包含股票代码的内容)
+ if news_items:
+ # 筛选包含股票代码的新闻
+ filtered_items = []
+ for item in news_items:
+ # 检查新闻内容是否包含股票代码
+ content = item.get('content', '')
+ title = item.get('title', '') if 'title' in item else ''
+ if symbol in content or symbol in title:
+ item['source'] = f"CCTV-{symbol}"
+ filtered_items.append(item)
- except Exception as e:
- print(f" ⚠ 从新浪财经获取失败: {e}")
+ # 如果找到了相关的个股新闻,则替换列表
+ if filtered_items:
+ news_items = filtered_items
+ print(f" ✓ 从CCTV中筛选出 {len(news_items)} 条与股票 {symbol} 相关的新闻")
- # 方法3: 尝试获取财联社电报
- if not news_items or len(news_items) < 5:
+ # 方法3: 尝试获取百度经济新闻
+ if not news_items:
try:
- # stock_news_cls() - 财联社电报
- df = ak.stock_news_cls()
+ # news_economic_baidu() - 百度经济新闻
+ df = ak.news_economic_baidu()
if df is not None and not df.empty:
- # 筛选包含股票代码或名称的新闻
- df_filtered = df[
- df['内容'].str.contains(symbol, na=False) |
- df['标题'].str.contains(symbol, na=False)
- ]
+ print(f" ✓ 从百度经济获取到 {len(df)} 条新闻")
- if not df_filtered.empty:
- print(f" ✓ 从财联社获取到 {len(df_filtered)} 条相关新闻")
+ for idx, row in df.head(self.max_items).iterrows():
+ item = {'source': '百度经济'}
+
+ for col in df.columns:
+ value = row.get(col)
+ if value is None or (isinstance(value, float) and pd.isna(value)):
+ continue
+ try:
+ item[col] = str(value)
+ except:
+ item[col] = "无法解析"
- for idx, row in df_filtered.head(self.max_items - len(news_items)).iterrows():
- item = {'source': '财联社'}
-
- for col in df_filtered.columns:
- value = row.get(col)
- if value is None or (isinstance(value, float) and pd.isna(value)):
- continue
- try:
- item[col] = str(value)
- except:
- item[col] = "无法解析"
-
- if len(item) > 1:
- news_items.append(item)
+ if len(item) > 1:
+ news_items.append(item)
except Exception as e:
- print(f" ⚠ 从财联社获取失败: {e}")
+ print(f" ⚠ 从百度经济获取失败: {e}")
if not news_items:
print(f" 未找到股票 {symbol} 的新闻")
diff --git a/risk_data_debug_output.txt b/risk_data_debug_output.txt
deleted file mode 100644
index d9c4e31..0000000
--- a/risk_data_debug_output.txt
+++ /dev/null
@@ -1,60 +0,0 @@
-================================================================================
-原始数据结构
-================================================================================
-
-data_success: True
-
-
-lifting_ban:
- has_data: True
- query: 300433限售解禁
- 记录数: 1
- 列名: ['table2', '解禁股数_选股表格', '解禁股数_回测点评', '解禁股数_事件K线图', '解禁股数_解禁时间表', '解禁股数_公告', '解禁股数_研报', '解禁股数_新闻']
-
-shareholder_reduction:
- has_data: True
- query: 300433大股东减持公告
- 记录数: 1
- 列名: ['title_content']
-
-important_events:
- has_data: True
- query: 300433近期重要事件
- 记录数: 1
- 列名: ['近期重要事件', '重要事件共用模块_简单表格', '重要事件共用模块_事件k线图']
-
-================================================================================
-格式化后传给AI的数据
-================================================================================
-
-================================================================================
-【限售解禁数据】
-================================================================================
-查询语句: 300433限售解禁
-
-共 1 条记录,显示前50条:
- table2 解禁股数_选股表格 解禁股数_回测点评 解禁股数_事件K线图 解禁股数_解禁时间表 解禁股数_公告 解禁股数_研报 解禁股数_新闻
-[{'实际解禁股数': 4557611, '解禁股类型': '股权激励限售股份', '是否公告值': '公布值', '股票简称': '蓝思科技', '解禁成本': 6.118285024553978, '实际解禁金额': 124650660.85000001, '解禁日期': '20251017', '解禁计算参考时间': '20251017', '时间区间': '20251017-20261016', '股票代码': '300433.SZ', '实际解禁比例': 0.09146810710188108}] {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 1}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 7619, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=7619&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 2}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 10667, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=10667&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 3}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 7621, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=7621&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 4}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9377, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9377&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 5}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9379, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9379&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 6}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9381, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9381&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '蓝思科技限售解禁', 'extra': {'source_key': 7}, 'logid': '3afe81ef36c9e73a908cfbfbd6ceeca9', 'pid': 9383, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251017","20261016"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251017%22%2C%2220261016%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=3afe81ef36c9e73a908cfbfbd6ceeca9&pid=9383&w=%E8%93%9D%E6%80%9D%E7%A7%91%E6%8A%80%E9%99%90%E5%94%AE%E8%A7%A3%E7%A6%81'}
-
-================================================================================
-【大股东减持数据】
-================================================================================
-查询语句: 300433大股东减持公告
-
-共 1 条记录,显示前50条:
- title_content
- uid ... publish_date
-0 bf08a24fbdf8d319 ... NaN
-1 1798c7717c8b9a90 ... 2025-10-15 08:47:10
-2 499bb08daa40cdf1 ... NaN
-
-[3 rows x 8 columns]
-
-================================================================================
-【重要事件数据】
-================================================================================
-查询语句: 300433近期重要事件
-
-共 1 条记录,显示前50条:
- 近期重要事件 重要事件共用模块_简单表格 重要事件共用模块_事件k线图
-[{'事件': '分红预案', '概要': '拟10派1元(含税)。', '日期': '20250826', '股票简称': '蓝思科技', '股票代码': '300433.SZ'}] {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '300433近期重要事件', 'extra': {'source_key': 1}, 'logid': 'c6b7099cfb37d5ae1c35b22f181e7cb0', 'pid': 13332, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251019","20251019"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251019%22%2C%2220251019%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=c6b7099cfb37d5ae1c35b22f181e7cb0&pid=13332&w=300433%E8%BF%91%E6%9C%9F%E9%87%8D%E8%A6%81%E4%BA%8B%E4%BB%B6'} {'meta': {'codes': '300433', 'codeType': 'stock', 'w': '300433近期重要事件', 'extra': {'source_key': 2}, 'logid': 'c6b7099cfb37d5ae1c35b22f181e7cb0', 'pid': 13333, 'info': '{"view":{"nolazy":1,"parseArr":{"_v":"new","dateRange":["20251019","20251019"],"staying":[],"queryCompare":[],"comparesOfIndex":[]}}}'}, 'url': '/diag/block-detail?codeType=stock&codes=300433&info=%7B%22view%22%3A%7B%22nolazy%22%3A1%2C%22parseArr%22%3A%7B%22_v%22%3A%22new%22%2C%22dateRange%22%3A%5B%2220251019%22%2C%2220251019%22%5D%2C%22staying%22%3A%5B%5D%2C%22queryCompare%22%3A%5B%5D%2C%22comparesOfIndex%22%3A%5B%5D%7D%7D%7D&logid=c6b7099cfb37d5ae1c35b22f181e7cb0&pid=13333&w=300433%E8%BF%91%E6%9C%9F%E9%87%8D%E8%A6%81%E4%BA%8B%E4%BB%B6'}
diff --git a/run.py b/run.py
index 9beee4e..4817e2b 100644
--- a/run.py
+++ b/run.py
@@ -52,14 +52,14 @@ def main():
# 启动Streamlit应用
print("🌐 正在启动Web界面...")
- print("📝 访问地址: http://localhost:8503")
+ print("📝 访问地址: http://localhost:8504")
print("⏹️ 按 Ctrl+C 停止服务")
print("=" * 50)
try:
subprocess.run([
sys.executable, "-m", "streamlit", "run", "app.py",
- "--server.port", "8503",
+ "--server.port", "8504",
"--server.address", "127.0.0.1"
])
except KeyboardInterrupt:
diff --git a/sector_strategy.db b/sector_strategy.db
index c092157..a9525b8 100644
Binary files a/sector_strategy.db and b/sector_strategy.db differ
diff --git a/sector_strategy_agents.py b/sector_strategy_agents.py
index 6383ba9..4e3b500 100644
--- a/sector_strategy_agents.py
+++ b/sector_strategy_agents.py
@@ -46,13 +46,19 @@ def macro_strategist_agent(self, market_data: Dict, news_data: list) -> Dict[str
"""
if market_data.get("sh_index"):
sh = market_data["sh_index"]
- market_summary += f" 上证指数: {sh['close']} ({sh['change_pct']:+.2f}%)\n"
+ close_price = sh.get('close', sh.get('最新价', 0))
+ change_pct = sh.get('change_pct', sh.get('涨跌幅', 0))
+ market_summary += f" 上证指数: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("sz_index"):
sz = market_data["sz_index"]
- market_summary += f" 深证成指: {sz['close']} ({sz['change_pct']:+.2f}%)\n"
+ close_price = sz.get('close', sz.get('最新价', 0))
+ change_pct = sz.get('change_pct', sz.get('涨跌幅', 0))
+ market_summary += f" 深证成指: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("cyb_index"):
cyb = market_data["cyb_index"]
- market_summary += f" 创业板指: {cyb['close']} ({cyb['change_pct']:+.2f}%)\n"
+ close_price = cyb.get('close', cyb.get('最新价', 0))
+ change_pct = cyb.get('change_pct', cyb.get('涨跌幅', 0))
+ market_summary += f" 创业板指: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("total_stocks"):
market_summary += f"""
@@ -143,13 +149,23 @@ def sector_diagnostician_agent(self, sectors_data: Dict, concepts_data: Dict, ma
涨幅榜 TOP15:
"""
for idx, (name, info) in enumerate(sorted_sectors[:15], 1):
- sector_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {info['turnover']:.2f}% | 领涨股: {info['top_stock']} ({info['top_stock_change']:+.2f}%) | 涨跌家数: {info['up_count']}/{info['down_count']}\n"
+ top_stock = info.get('top_stock', info.get('领涨股票', '暂无'))
+ top_stock_change = info.get('top_stock_change', info.get('领涨股票涨跌幅', 0))
+ turnover = info.get('turnover', info.get('换手率', 0))
+ up_count = info.get('up_count', info.get('上涨家数', 0))
+ down_count = info.get('down_count', info.get('下跌家数', 0))
+ sector_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {turnover:.2f}% | 领涨股: {top_stock} ({top_stock_change:+.2f}%) | 涨跌家数: {up_count}/{down_count}\n"
sector_summary += f"""
跌幅榜 TOP10:
"""
for idx, (name, info) in enumerate(sorted_sectors[-10:], 1):
- sector_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {info['turnover']:.2f}% | 领跌股: {info['top_stock']} ({info['top_stock_change']:+.2f}%) | 涨跌家数: {info['up_count']}/{info['down_count']}\n"
+ top_stock = info.get('top_stock', info.get('领涨股票', '暂无'))
+ top_stock_change = info.get('top_stock_change', info.get('领涨股票涨跌幅', 0))
+ turnover = info.get('turnover', info.get('换手率', 0))
+ up_count = info.get('up_count', info.get('上涨家数', 0))
+ down_count = info.get('down_count', info.get('下跌家数', 0))
+ sector_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {turnover:.2f}% | 领跌股: {top_stock} ({top_stock_change:+.2f}%) | 涨跌家数: {up_count}/{down_count}\n"
# 构建概念板块数据
concept_summary = ""
@@ -162,7 +178,10 @@ def sector_diagnostician_agent(self, sectors_data: Dict, concepts_data: Dict, ma
热门概念 TOP15:
"""
for idx, (name, info) in enumerate(sorted_concepts[:15], 1):
- concept_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {info['turnover']:.2f}% | 领涨股: {info['top_stock']} ({info['top_stock_change']:+.2f}%)\n"
+ top_stock = info.get('top_stock', info.get('领涨股票', '暂无'))
+ top_stock_change = info.get('top_stock_change', info.get('领涨股票涨跌幅', 0))
+ turnover = info.get('turnover', info.get('换手率', 0))
+ concept_summary += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {turnover:.2f}% | 领涨股: {top_stock} ({top_stock_change:+.2f}%)\n"
prompt = f"""
你是一名资深的板块分析师,具有CFA资格和深厚的行业研究背景,擅长板块诊断和趋势判断。
@@ -257,7 +276,9 @@ def fund_flow_analyst_agent(self, fund_flow_data: Dict, north_flow_data: Dict, s
主力资金净流入 TOP15:
"""
for idx, item in enumerate(sorted_inflow[:15], 1):
- fund_flow_summary += f"{idx}. {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {item['change_pct']:+.2f}% | 超大单: {item['super_large_net_inflow']:.2f}万\n"
+ # 兼容性处理:获取涨跌幅字段
+ change_pct = item.get('change_pct', item.get('今日涨跌幅', 0))
+ fund_flow_summary += f"{idx}. {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {change_pct:+.2f}% | 超大单: {item['super_large_net_inflow']:.2f}万\n"
# 净流出前10
sorted_outflow = sorted(flow_list, key=lambda x: x["main_net_inflow"])
@@ -265,7 +286,9 @@ def fund_flow_analyst_agent(self, fund_flow_data: Dict, north_flow_data: Dict, s
主力资金净流出 TOP10:
"""
for idx, item in enumerate(sorted_outflow[:10], 1):
- fund_flow_summary += f"{idx}. {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {item['change_pct']:+.2f}%\n"
+ # 兼容性处理:获取涨跌幅字段
+ change_pct = item.get('change_pct', item.get('今日涨跌幅', 0))
+ fund_flow_summary += f"{idx}. {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {change_pct:+.2f}%\n"
# 构建北向资金数据
north_summary = ""
@@ -273,14 +296,14 @@ def fund_flow_analyst_agent(self, fund_flow_data: Dict, north_flow_data: Dict, s
north_summary = f"""
【北向资金】
日期: {north_flow_data.get('date', 'N/A')}
-今日北向资金净流入: {north_flow_data.get('north_net_inflow', 0):.2f} 万元
- 沪股通净流入: {north_flow_data.get('hgt_net_inflow', 0):.2f} 万元
- 深股通净流入: {north_flow_data.get('sgt_net_inflow', 0):.2f} 万元
+今日北向资金净流入: {north_flow_data.get('north_net_inflow', 0):.2f} 亿元
+ 沪股通净流入: {north_flow_data.get('hgt_net_inflow', 0):.2f} 亿元
+ 深股通净流入: {north_flow_data.get('sgt_net_inflow', 0):.2f} 亿元
"""
if north_flow_data.get('history'):
- north_summary += "\n近10日北向资金流向:\n"
- for item in north_flow_data['history'][:10]:
- north_summary += f" {item['date']}: {item['net_inflow']:.2f}万\n"
+ north_summary += "\n近20日北向资金流向:\n"
+ for item in north_flow_data['history'][:20]:
+ north_summary += f" {item['date']}: {item['net_inflow']:.2f}亿\n"
prompt = f"""
你是一名资深的资金流向分析师,拥有15年的市场资金研究经验,擅长从资金流向中洞察主力意图和市场趋势。
@@ -388,13 +411,19 @@ def market_sentiment_decoder_agent(self, market_data: Dict, sectors_data: Dict,
"""
if market_data.get("sh_index"):
sh = market_data["sh_index"]
- sentiment_summary += f" 上证指数: {sh['close']} ({sh['change_pct']:+.2f}%)\n"
+ close_price = sh.get('close', sh.get('最新价', 0))
+ change_pct = sh.get('change_pct', sh.get('涨跌幅', 0))
+ sentiment_summary += f" 上证指数: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("sz_index"):
sz = market_data["sz_index"]
- sentiment_summary += f" 深证成指: {sz['close']} ({sz['change_pct']:+.2f}%)\n"
+ close_price = sz.get('close', sz.get('最新价', 0))
+ change_pct = sz.get('change_pct', sz.get('涨跌幅', 0))
+ sentiment_summary += f" 深证成指: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("cyb_index"):
cyb = market_data["cyb_index"]
- sentiment_summary += f" 创业板指: {cyb['close']} ({cyb['change_pct']:+.2f}%)\n"
+ close_price = cyb.get('close', cyb.get('最新价', 0))
+ change_pct = cyb.get('change_pct', cyb.get('涨跌幅', 0))
+ sentiment_summary += f" 创业板指: {close_price} ({change_pct:+.2f}%)\n"
# 板块热度分析
hot_sectors = ""
@@ -406,19 +435,30 @@ def market_sentiment_decoder_agent(self, market_data: Dict, sectors_data: Dict,
最活跃板块 TOP10:
"""
for idx, (name, info) in enumerate(sorted_sectors[:10], 1):
- hot_sectors += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {info['turnover']:.2f}% | 涨跌家数: {info['up_count']}/{info['down_count']}\n"
+ # 兼容性处理:获取涨跌幅字段
+ change_pct = info.get('change_pct', info.get('涨跌幅', 0))
+ turnover = info.get('turnover', info.get('换手率', 0))
+ up_count = info.get('up_count', info.get('上涨家数', 0))
+ down_count = info.get('down_count', info.get('下跌家数', 0))
+ hot_sectors += f"{idx}. {name}: {change_pct:+.2f}% | 换手率: {turnover:.2f}% | 涨跌家数: {up_count}/{down_count}\n"
# 概念热度
hot_concepts = ""
if concepts_data:
- sorted_concepts = sorted(concepts_data.items(), key=lambda x: abs(x[1]["change_pct"]), reverse=True)
+ # 兼容性处理:排序时使用兼容的字段访问
+ sorted_concepts = sorted(concepts_data.items(),
+ key=lambda x: abs(x[1].get('change_pct', x[1].get('涨跌幅', 0))),
+ reverse=True)
hot_concepts = f"""
【概念热度排行】
最热概念 TOP10:
"""
for idx, (name, info) in enumerate(sorted_concepts[:10], 1):
- hot_concepts += f"{idx}. {name}: {info['change_pct']:+.2f}% | 换手率: {info['turnover']:.2f}%\n"
+ # 兼容性处理:获取涨跌幅字段
+ change_pct = info.get('change_pct', info.get('涨跌幅', 0))
+ turnover = info.get('turnover', info.get('换手率', 0))
+ hot_concepts += f"{idx}. {name}: {change_pct:+.2f}% | 换手率: {turnover:.2f}%\n"
prompt = f"""
你是一名资深的市场情绪分析师,拥有心理学和金融学双重背景,擅长从市场数据中解读投资者情绪和市场心理。
@@ -507,10 +547,14 @@ def _format_market_overview(self, market_data):
text = ""
if market_data.get("sh_index"):
sh = market_data["sh_index"]
- text += f"上证指数: {sh['close']} ({sh['change_pct']:+.2f}%)\n"
+ close_price = sh.get('close', sh.get('最新价', 0))
+ change_pct = sh.get('change_pct', sh.get('涨跌幅', 0))
+ text += f"上证指数: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("sz_index"):
sz = market_data["sz_index"]
- text += f"深证成指: {sz['close']} ({sz['change_pct']:+.2f}%)\n"
+ close_price = sz.get('close', sz.get('最新价', 0))
+ change_pct = sz.get('change_pct', sz.get('涨跌幅', 0))
+ text += f"深证成指: {close_price} ({change_pct:+.2f}%)\n"
if market_data.get("total_stocks"):
text += f"涨跌统计: 上涨{market_data['up_count']}只({market_data['up_ratio']:.1f}%),下跌{market_data['down_count']}只\n"
diff --git a/sector_strategy_data.py b/sector_strategy_data.py
index 0df19c1..2cc2c4a 100644
--- a/sector_strategy_data.py
+++ b/sector_strategy_data.py
@@ -75,58 +75,65 @@ def get_all_sector_data(self):
"news": []
}
- try:
- # 1. 获取行业板块数据
- print(" [1/6] 获取行业板块行情...")
- sectors_data = self._get_sector_performance()
- if sectors_data:
- data["sectors"] = sectors_data
- print(f" ✓ 成功获取 {len(sectors_data)} 个行业板块数据")
-
- # 2. 获取概念板块数据
- print(" [2/6] 获取概念板块行情...")
- concept_data = self._get_concept_performance()
- if concept_data:
- data["concepts"] = concept_data
- print(f" ✓ 成功获取 {len(concept_data)} 个概念板块数据")
-
- # 3. 获取板块资金流向
- print(" [3/6] 获取行业资金流向...")
- fund_flow_data = self._get_sector_fund_flow()
- if fund_flow_data:
- data["sector_fund_flow"] = fund_flow_data
- print(f" ✓ 成功获取资金流向数据")
-
- # 4. 获取市场总体情况
- print(" [4/6] 获取市场总体情况...")
- market_data = self._get_market_overview()
- if market_data:
- data["market_overview"] = market_data
- print(f" ✓ 成功获取市场概况")
-
- # 5. 获取北向资金流向
- print(" [5/6] 获取北向资金流向...")
- north_flow = self._get_north_money_flow()
- if north_flow:
- data["north_flow"] = north_flow
- print(f" ✓ 成功获取北向资金数据")
-
- # 6. 获取财经新闻
- print(" [6/6] 获取财经新闻...")
- news_data = self._get_financial_news()
- if news_data:
- data["news"] = news_data
- print(f" ✓ 成功获取 {len(news_data)} 条新闻")
-
- data["success"] = True
+ # 1. 获取行业板块数据
+ print(" [1/6] 获取行业板块行情...")
+ sectors_data = self._get_sector_performance()
+ if sectors_data:
+ data["sectors"] = sectors_data
+ print(f" ✓ 成功获取 {len(sectors_data)} 个行业板块数据")
+
+ # 2. 获取概念板块数据
+ print(" [2/6] 获取概念板块行情...")
+ concept_data = self._get_concept_performance()
+ if concept_data:
+ data["concepts"] = concept_data
+ print(f" ✓ 成功获取 {len(concept_data)} 个概念板块数据")
+
+ # 3. 获取板块资金流向
+ print(" [3/6] 获取行业资金流向...")
+ fund_flow_data = self._get_sector_fund_flow()
+ if fund_flow_data:
+ data["sector_fund_flow"] = fund_flow_data
+ print(f" ✓ 成功获取资金流向数据")
+
+ # 4. 获取市场总体情况
+ print(" [4/6] 获取市场总体情况...")
+ market_data = self._get_market_overview()
+ if market_data:
+ data["market_overview"] = market_data
+ print(f" ✓ 成功获取市场概况")
+
+ # 5. 获取北向资金流向
+ print(" [5/6] 获取北向资金流向...")
+ north_flow = self._get_north_money_flow()
+ if north_flow:
+ data["north_flow"] = north_flow
+ print(f" ✓ 成功获取北向资金数据")
+
+ # 6. 获取财经新闻
+ print(" [6/6] 获取财经新闻...")
+ news_data = self._get_financial_news()
+ if news_data:
+ data["news"] = news_data
+ print(f" ✓ 成功获取 {len(news_data)} 条新闻")
+
+ # 检查是否有任何数据成功获取
+ has_data = (
+ data["sectors"] or
+ data.get("concepts", {}) or
+ data["sector_fund_flow"] or
+ data["market_overview"] or
+ data["north_flow"] or
+ data["news"]
+ )
+
+ data["success"] = bool(has_data)
+ if has_data:
print("[智策] ✓ 板块数据获取完成!")
-
# 保存原始数据到数据库
self._save_raw_data_to_db(data)
-
- except Exception as e:
- print(f"[智策] ✗ 数据获取出错: {e}")
- data["error"] = str(e)
+ else:
+ print("[智策] ⚠ 未能获取任何数据")
return data
@@ -137,6 +144,12 @@ def _get_sector_performance(self):
df = self._safe_request(ak.stock_board_industry_name_em)
if df is None or df.empty:
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载行业板块数据...")
+ cached_data = self.database.get_latest_raw_data("sectors")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载行业板块缓存数据")
+ return cached_data.get("data_content", {})
return {}
# 转换为字典格式
@@ -159,6 +172,15 @@ def _get_sector_performance(self):
except Exception as e:
print(f" 获取行业板块数据失败: {e}")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载行业板块数据...")
+ try:
+ cached_data = self.database.get_latest_raw_data("sectors")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载行业板块缓存数据")
+ return cached_data.get("data_content", {})
+ except Exception as cache_error:
+ print(f" [缓存] 加载行业板块缓存数据失败: {cache_error}")
return {}
def _get_concept_performance(self):
@@ -168,6 +190,12 @@ def _get_concept_performance(self):
df = self._safe_request(ak.stock_board_concept_name_em)
if df is None or df.empty:
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载概念板块数据...")
+ cached_data = self.database.get_latest_raw_data("concepts")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载概念板块缓存数据")
+ return cached_data.get("data_content", {})
return {}
# 转换为字典格式
@@ -190,6 +218,15 @@ def _get_concept_performance(self):
except Exception as e:
print(f" 获取概念板块数据失败: {e}")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载概念板块数据...")
+ try:
+ cached_data = self.database.get_latest_raw_data("concepts")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载概念板块缓存数据")
+ return cached_data.get("data_content", {})
+ except Exception as cache_error:
+ print(f" [缓存] 加载概念板块缓存数据失败: {cache_error}")
return {}
def _get_sector_fund_flow(self):
@@ -199,6 +236,12 @@ def _get_sector_fund_flow(self):
df = self._safe_request(ak.stock_sector_fund_flow_rank, indicator="今日")
if df is None or df.empty:
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载行业资金流向数据...")
+ cached_data = self.database.get_latest_raw_data("fund_flow")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载行业资金流向缓存数据")
+ return cached_data.get("data_content", {})
return {}
# 转换为字典格式
@@ -223,6 +266,15 @@ def _get_sector_fund_flow(self):
except Exception as e:
print(f" 获取行业资金流向失败: {e}")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载行业资金流向数据...")
+ try:
+ cached_data = self.database.get_latest_raw_data("fund_flow")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载行业资金流向缓存数据")
+ return cached_data.get("data_content", {})
+ except Exception as cache_error:
+ print(f" [缓存] 加载行业资金流向缓存数据失败: {cache_error}")
return {}
def _get_market_overview(self):
@@ -291,84 +343,167 @@ def _get_market_overview(self):
except:
pass
+ # 如果没有获取到足够的数据,尝试从缓存加载
+ if not overview:
+ print(" [缓存] 尝试从缓存加载市场概况数据...")
+ cached_data = self.database.get_latest_raw_data("market_overview")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载市场概况缓存数据")
+ return cached_data.get("data_content", {})
+
return overview
except Exception as e:
print(f" 获取市场概况失败: {e}")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载市场概况数据...")
+ try:
+ cached_data = self.database.get_latest_raw_data("market_overview")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载市场概况缓存数据")
+ return cached_data.get("data_content", {})
+ except Exception as cache_error:
+ print(f" [缓存] 加载市场概况缓存数据失败: {cache_error}")
return {}
def _get_north_money_flow(self):
- """获取北向资金流向(优先使用Tushare,失败时使用Akshare)"""
- # 优先使用Tushare获取沪深港通资金流向
- self.ts_pro = None
- tushare_token = os.getenv('TUSHARE_TOKEN', '')
+ """获取北向资金流向(优先使用手动输入数据,然后Tushare,最后使用Akshare)"""
+
+ # 优先检查是否有手动输入的数据
+ if hasattr(self, 'manual_north_data') and self.manual_north_data is not None:
+ try:
+ print(" [手动数据] 正在使用手动输入的北向资金数据...")
+ return self._process_manual_north_data()
+ except Exception as e:
+ print(f" [手动数据] 处理失败: {e}")
+ # 继续尝试其他数据源
+
+ # 检查session state中的手动数据(用于Streamlit界面)
try:
- # 初始化Tushare(如果尚未初始化)
- if not hasattr(self, '_tushare_api'):
- TUSHARE_TOKEN = os.getenv('TUSHARE_TOKEN', '')
- if TUSHARE_TOKEN:
- try:
- import tushare as ts
- ts.set_token(tushare_token)
- self.ts_pro = ts.pro_api()
- print(" [Tushare] ✅ 初始化成功")
- except Exception as e:
- print(f" [Tushare] 初始化失败: {e}")
- self._tushare_api = None
- else:
- print(" [Tushare] 未配置Token")
- self._tushare_api = None
-
+ import streamlit as st
+ if hasattr(st, 'session_state') and 'manual_north_data' in st.session_state:
+ manual_data = st.session_state.manual_north_data
+ if manual_data is not None and not manual_data.empty:
+ print(" [手动数据] 正在使用界面输入的北向资金数据...")
+ return self._process_manual_north_data(manual_data)
+ except Exception as e:
+ print(f" [手动数据] 从界面获取数据失败: {e}")
+
+ # 优先使用Tushare获取沪深港通资金流向(无法获取近期数据,改用手动输入)
+ # try:
+ # # 初始化Tushare(如果尚未初始化)
+ # if not hasattr(self, 'ts_pro') or self.ts_pro is None:
+ # tushare_token = os.getenv('TUSHARE_TOKEN', '')
+ # if tushare_token:
+ # try:
+ # import tushare as ts
+ # ts.set_token(tushare_token)
+ # self.ts_pro = ts.pro_api()
+ # print(" [Tushare] ✅ 初始化成功")
+ # except Exception as e:
+ # print(f" [Tushare] 初始化失败: {e}")
+ # self.ts_pro = None
+ # else:
+ # print(" [Tushare] 未配置Token")
+ # self.ts_pro = None
- # 如果Tushare可用,获取数据
- if hasattr(self, '_tushare_api') and self._tushare_api:
- print(" [Tushare] 正在获取沪深港通资金流向...")
+ # # 如果Tushare可用,获取数据
+ # if hasattr(self, 'ts_pro') and self.ts_pro is not None:
+ # print(" [Tushare] 正在获取沪深港通资金流向...")
- # 获取最近30天的数据
- end_date = datetime.now()
- start_date = end_date - timedelta(days=20)
+ # # 直接请求过去一个月的数据范围
+ # end_date = datetime.now()
+ # # 如果当前时间在00:00-21:00之间,将end_date设为前一天,避免当日数据未更新
+ # if 0 <= end_date.hour < 21:
+ # end_date = end_date - timedelta(days=1)
+ # start_date = end_date - timedelta(days=30) # 过去30天(一个月)
- df = self._tushare_api.moneyflow_hsgt(
- start_date=start_date.strftime('%Y%m%d'),
- end_date=end_date.strftime('%Y%m%d')
- )
+ # # 格式化日期用于日志输出
+ # start_date_str = start_date.strftime('%Y%m%d')
+ # end_date_str = end_date.strftime('%Y%m%d')
- if df is not None and not df.empty:
- print(" [Tushare] ✅ 成功获取数据")
-
- # 按日期降序排列,获取最新数据
- df = df.sort_values('trade_date', ascending=False)
- latest = df.iloc[0]
-
- # 转换数据格式以匹配原有结构
- north_flow = {
- "date": str(latest['trade_date']),
- "north_net_inflow": float(latest['north_money']),
- "hgt_net_inflow": float(latest['hgt']),
- "sgt_net_inflow": float(latest['sgt']),
- "north_total_amount": float(latest['north_money']) # Tushare没有总成交金额,使用净流入作为近似值
- }
-
- # 获取历史趋势(最近20天)
- history = []
- for idx, row in df.head(20).iterrows():
- history.append({
- "date": str(row['trade_date']),
- "net_inflow": float(row['north_money'])
- })
- north_flow["history"] = history
+ # # 添加日志显示格式化后的日期
+ # print(f" [Tushare] 请求日期范围: {start_date_str} 至 {end_date_str}")
+
+ # # 尝试多种查询策略
+ # df = None
+
+ # # 策略1:范围查询
+ # try:
+ # df = self.ts_pro.moneyflow_hsgt(
+ # start_date=start_date.strftime('%Y%m%d'),
+ # end_date=end_date.strftime('%Y%m%d')
+ # )
+ # if df is not None and not df.empty:
+ # print(f" [Tushare] ✅ 范围查询成功,共 {len(df)} 条记录")
+ # else:
+ # print(" [Tushare] ❌ 范围查询未获取到数据")
+ # except Exception as range_error:
+ # print(f" [Tushare] 范围查询失败: {range_error}")
+ # df = None
+
+ # # 策略2:如果范围查询失败,尝试单日查询(最近几个交易日)
+ # if df is None or df.empty:
+ # print(" [Tushare] 尝试单日查询...")
+ # for i in range(10): # 尝试最近10天
+ # test_date = (end_date - timedelta(days=i)).strftime('%Y%m%d')
+ # try:
+ # df_single = self.ts_pro.moneyflow_hsgt(trade_date=test_date)
+ # if df_single is not None and not df_single.empty:
+ # print(f" [Tushare] ✅ 单日查询成功 ({test_date}),共 {len(df_single)} 条记录")
+ # df = df_single
+ # break
+ # except Exception as single_error:
+ # continue
+
+ # # 处理获取到的数据
+ # if df is not None and not df.empty:
+ # # 确保必要的列存在
+ # required_columns = ['trade_date', 'north_money', 'hgt', 'sgt']
+ # for col in required_columns:
+ # if col not in df.columns:
+ # print(f" [Tushare] ❌ 缺少必要列: {col}")
+ # df = None
+ # break
- return north_flow
- else:
- print(" [Tushare] ❌ 未获取到数据")
- else:
- print(" [Tushare] 不可用")
- except Exception as e:
- print(f" [Tushare] 获取北向资金失败: {e}")
+ # if df is not None:
+ # # 按日期降序排列,获取最新数据
+ # df = df.sort_values('trade_date', ascending=False)
+ # latest = df.iloc[0]
+
+ # # 转换数据格式以匹配原有结构
+ # north_flow = {
+ # "date": str(latest['trade_date']),
+ # "north_net_inflow": float(latest['north_money']) if pd.notna(latest['north_money']) else 0.0,
+ # "hgt_net_inflow": float(latest['hgt']) if pd.notna(latest['hgt']) else 0.0,
+ # "sgt_net_inflow": float(latest['sgt']) if pd.notna(latest['sgt']) else 0.0,
+ # "north_total_amount": float(latest['north_money']) if pd.notna(latest['north_money']) else 0.0 # Tushare没有总成交金额,使用净流入作为近似值
+ # }
+
+ # # 获取历史趋势(最近20天)
+ # history = []
+ # for idx, row in df.head(20).iterrows():
+ # history.append({
+ # "date": str(row['trade_date']),
+ # "net_inflow": float(row['north_money']) if pd.notna(row['north_money']) else 0.0
+ # })
+ # north_flow["history"] = history
+
+ # return north_flow
+
+ # print(" [Tushare] ❌ 未获取到数据(可能是非交易时间、积分不足或访问频率超限)")
+ # else:
+ # print(" [Tushare] 不可用")
+ # except Exception as api_error:
+ # print(f" [Tushare] API调用失败: {api_error}")
+ # import traceback
+ # traceback.print_exc()
# Tushare失败,尝试使用Akshare
try:
print(" [Akshare] 正在获取沪深港通资金流向(备用数据源)...")
+ # 延迟导入akshare,避免不必要的依赖
+ import akshare as ak
df = self._safe_request(ak.stock_hsgt_fund_flow_summary_em)
if df is not None and not df.empty:
@@ -379,10 +514,10 @@ def _get_north_money_flow(self):
north_flow = {
"date": str(latest.get('日期', '')),
- "north_net_inflow": latest.get('北向资金-成交净买额', 0),
- "hgt_net_inflow": latest.get('沪股通-成交净买额', 0),
- "sgt_net_inflow": latest.get('深股通-成交净买额', 0),
- "north_total_amount": latest.get('北向资金-成交金额', 0)
+ "north_net_inflow": float(latest.get('北向资金-成交净买额', 0)) if pd.notna(latest.get('北向资金-成交净买额', 0)) else 0.0,
+ "hgt_net_inflow": float(latest.get('沪股通-成交净买额', 0)) if pd.notna(latest.get('沪股通-成交净买额', 0)) else 0.0,
+ "sgt_net_inflow": float(latest.get('深股通-成交净买额', 0)) if pd.notna(latest.get('深股通-成交净买额', 0)) else 0.0,
+ "north_total_amount": float(latest.get('北向资金-成交金额', 0)) if pd.notna(latest.get('北向资金-成交金额', 0)) else 0.0
}
# 获取历史趋势(最近20天)
@@ -390,20 +525,102 @@ def _get_north_money_flow(self):
for idx, row in df.head(20).iterrows():
history.append({
"date": str(row.get('日期', '')),
- "net_inflow": row.get('北向资金-成交净买额', 0)
+ "net_inflow": float(row.get('北向资金-成交净买额', 0)) if pd.notna(row.get('北向资金-成交净买额', 0)) else 0.0
})
north_flow["history"] = history
return north_flow
else:
print(" [Akshare] ❌ 未获取到数据")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载北向资金数据...")
+ cached_data = self.database.get_latest_raw_data("north_flow")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载北向资金缓存数据")
+ return cached_data.get("data_content", {})
except Exception as e:
print(f" [Akshare] 获取北向资金失败: {e}")
+ import traceback
+ traceback.print_exc()
+
+ # 所有数据源都失败,尝试从缓存加载
+ print(" [缓存] 尝试从缓存加载北向资金数据...")
+ try:
+ cached_data = self.database.get_latest_raw_data("north_flow")
+ if cached_data:
+ print(" [缓存] ✓ 成功加载北向资金缓存数据")
+ return cached_data.get("data_content", {})
+ except Exception as cache_error:
+ print(f" [缓存] 加载北向资金缓存数据失败: {cache_error}")
- # 所有数据源都失败
print(" ❌ 所有数据源均获取失败")
return {}
+ def _process_manual_north_data(self, manual_data=None):
+ """处理手动输入的北向资金数据"""
+ try:
+ # 如果没有传入数据,使用实例属性
+ if manual_data is None:
+ manual_data = getattr(self, 'manual_north_data', None)
+
+ if manual_data is None or manual_data.empty:
+ print(" [手动数据] ❌ 没有可用的手动数据")
+ return {}
+
+ print(f" [手动数据] ✅ 处理 {len(manual_data)} 条记录")
+
+ # 按日期排序,获取最新数据
+ manual_data = manual_data.sort_values('日期', ascending=False)
+ latest = manual_data.iloc[0]
+
+ # 数据格式转换
+ def safe_convert_to_float(value):
+ """安全转换为浮点数,处理带单位的字符串"""
+ if pd.isna(value):
+ return 0.0
+ if isinstance(value, str):
+ # 移除"亿元"等单位
+ value = value.replace('亿元', '').replace('亿', '').replace('元', '').replace(',', '').strip()
+ if value == '' or value == '-':
+ return 0.0
+ try:
+ return float(value)
+ except (ValueError, TypeError):
+ return 0.0
+
+ # 构建返回数据
+ north_flow = {
+ "date": latest['日期'].strftime('%Y%m%d') if hasattr(latest['日期'], 'strftime') else str(latest['日期']),
+ "north_net_inflow": safe_convert_to_float(latest.get('北向成交总额', 0)), # 使用成交总额作为净流入近似值
+ "hgt_net_inflow": safe_convert_to_float(latest.get('沪股通', 0)),
+ "sgt_net_inflow": safe_convert_to_float(latest.get('深股通', 0)),
+ "north_total_amount": safe_convert_to_float(latest.get('北向成交总额', 0))
+ }
+
+ # 构建历史趋势数据
+ history = []
+ for idx, row in manual_data.head(20).iterrows():
+ history.append({
+ "date": row['日期'].strftime('%Y%m%d') if hasattr(row['日期'], 'strftime') else str(row['日期']),
+ "net_inflow": safe_convert_to_float(row.get('北向成交总额', 0))
+ })
+ north_flow["history"] = history
+
+ print(f" [手动数据] ✅ 成功处理数据,最新日期: {north_flow['date']}")
+ return north_flow
+
+ except Exception as e:
+ print(f" [手动数据] 处理失败: {e}")
+ import traceback
+ traceback.print_exc()
+ return {}
+
+ def set_manual_north_data(self, data):
+ """设置手动输入的北向资金数据"""
+ self.manual_north_data = data
+ print(f" [手动数据] 已设置 {len(data) if data is not None else 0} 条记录")
+
+
def _get_financial_news(self):
"""获取财经新闻"""
try:
@@ -411,6 +628,13 @@ def _get_financial_news(self):
df = self._safe_request(ak.stock_news_em, symbol="全球")
if df is None or df.empty:
+ print(" [Akshare] ❌ 未获取到财经新闻数据")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载财经新闻数据...")
+ cached_data = self.database.get_latest_news_data()
+ if cached_data:
+ print(" [缓存] ✓ 成功加载财经新闻缓存数据")
+ return cached_data.get("news_content", [])
return []
news_list = []
@@ -427,6 +651,15 @@ def _get_financial_news(self):
except Exception as e:
print(f" 获取财经新闻失败: {e}")
+ # 尝试从缓存加载数据
+ print(" [缓存] 尝试从缓存加载财经新闻数据...")
+ try:
+ cached_data = self.database.get_latest_news_data()
+ if cached_data:
+ print(" [缓存] ✓ 成功加载财经新闻缓存数据")
+ return cached_data.get("news_content", [])
+ except Exception as cache_error:
+ print(f" [缓存] 加载财经新闻缓存数据失败: {cache_error}")
return []
def format_data_for_ai(self, data):
@@ -447,16 +680,70 @@ def format_data_for_ai(self, data):
大盘指数:
""")
+ # 处理直接获取的数据结构
if market.get("sh_index"):
sh = market["sh_index"]
- text_parts.append(f" 上证指数: {sh['close']} ({sh['change_pct']:+.2f}%)")
+ # 处理不同数据结构的字段名
+ close_price = sh.get('close', sh.get('price', sh.get('最新价', 0)))
+ change_pct = sh.get('change_pct', sh.get('涨跌幅', 0))
+ text_parts.append(f" 上证指数: {close_price} ({change_pct:+.2f}%)")
if market.get("sz_index"):
sz = market["sz_index"]
- text_parts.append(f" 深证成指: {sz['close']} ({sz['change_pct']:+.2f}%)")
+ # 处理不同数据结构的字段名
+ close_price = sz.get('close', sz.get('price', sz.get('最新价', 0)))
+ change_pct = sz.get('change_pct', sz.get('涨跌幅', 0))
+ text_parts.append(f" 深证成指: {close_price} ({change_pct:+.2f}%)")
if market.get("cyb_index"):
cyb = market["cyb_index"]
- text_parts.append(f" 创业板指: {cyb['close']} ({cyb['change_pct']:+.2f}%)")
+ # 处理不同数据结构的字段名
+ close_price = cyb.get('close', cyb.get('price', cyb.get('最新价', 0)))
+ change_pct = cyb.get('change_pct', cyb.get('涨跌幅', 0))
+ text_parts.append(f" 创业板指: {close_price} ({change_pct:+.2f}%)")
+
+ # 处理从缓存加载的数据结构
+ if not market.get("sh_index") and not market.get("sz_index") and not market.get("cyb_index"):
+ # 如果是缓存数据,它可能是一个包含上证、深证、创业板指数据的列表
+ try:
+ # 尝试处理缓存数据结构
+ if isinstance(market, dict) and "data_content" in market:
+ # 如果是完整的缓存数据对象
+ market_data = market["data_content"]
+ else:
+ # 如果是直接的缓存数据
+ market_data = market
+
+ # 如果是DataFrame或类似结构
+ if isinstance(market_data, dict) and market_data:
+ # 检查是否是DataFrame转换的字典
+ for item in market_data.get("data", []):
+ if isinstance(item, dict):
+ name = item.get("名称", "")
+ close = item.get("最新价", 0)
+ change_pct = item.get("涨跌幅", 0)
+ if "上证" in name:
+ text_parts.append(f" 上证指数: {close} ({change_pct:+.2f}%)")
+ elif "深证" in name:
+ text_parts.append(f" 深证成指: {close} ({change_pct:+.2f}%)")
+ elif "创业板" in name:
+ text_parts.append(f" 创业板指: {close} ({change_pct:+.2f}%)")
+ elif isinstance(market_data, list) and market_data:
+ # 如果是列表形式的缓存数据
+ for item in market_data:
+ if isinstance(item, dict):
+ name = item.get("名称", "")
+ close = item.get("最新价", 0)
+ change_pct = item.get("涨跌幅", 0)
+ if "上证" in name:
+ text_parts.append(f" 上证指数: {close} ({change_pct:+.2f}%)")
+ elif "深证" in name:
+ text_parts.append(f" 深证成指: {close} ({change_pct:+.2f}%)")
+ elif "创业板" in name:
+ text_parts.append(f" 创业板指: {close} ({change_pct:+.2f}%)")
+ except Exception as e:
+ # 如果处理缓存数据失败,至少显示有市场数据
+ text_parts.append(" 市场数据: 已获取但格式不兼容")
+ # 处理市场统计信息
if market.get("total_stocks"):
text_parts.append(f"""
市场统计:
@@ -467,6 +754,17 @@ def format_data_for_ai(self, data):
涨停: {market['limit_up']}
跌停: {market['limit_down']}
""")
+ # 处理缓存数据中的市场统计信息
+ elif isinstance(market, dict) and "total_stocks" in market:
+ text_parts.append(f"""
+市场统计:
+ 总股票数: {market['total_stocks']}
+ 上涨: {market['up_count']} ({market['up_ratio']:.1f}%)
+ 下跌: {market['down_count']}
+ 平盘: {market['flat_count']}
+ 涨停: {market['limit_up']}
+ 跌停: {market['limit_down']}
+""")
# 北向资金
if data.get("north_flow"):
@@ -474,52 +772,78 @@ def format_data_for_ai(self, data):
text_parts.append(f"""
【北向资金流向】
日期: {north.get('date', 'N/A')}
-北向资金净流入: {north.get('north_net_inflow', 0):.2f} 万元
- 沪股通: {north.get('hgt_net_inflow', 0):.2f} 万元
- 深股通: {north.get('sgt_net_inflow', 0):.2f} 万元
+北向资金净流入: {north.get('north_net_inflow', 0):.2f} 亿元
+ 沪股通: {north.get('hgt_net_inflow', 0):.2f} 亿元
+ 深股通: {north.get('sgt_net_inflow', 0):.2f} 亿元
""")
+
+ # 添加历史趋势数据(如果存在)
+ if north.get('history'):
+ text_parts.append("\n近20日北向资金流向:")
+ for item in north['history'][:20]:
+ text_parts.append(f" {item.get('date', 'N/A')}: {item.get('net_inflow', 0):.2f}亿")
# 行业板块表现(前20)
- if data.get("sectors"):
- sectors = data["sectors"]
- sorted_sectors = sorted(sectors.items(), key=lambda x: x[1]["change_pct"], reverse=True)
-
- text_parts.append(f"""
+ if data.get("sectors"):
+ sectors = data["sectors"]
+ sorted_sectors = sorted(sectors.items(), key=lambda x: x[1]["change_pct"], reverse=True)
+
+ text_parts.append(f"""
【行业板块表现 TOP20】
涨幅榜前10:
""")
- for name, info in sorted_sectors[:10]:
- text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领涨: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
-
- text_parts.append(f"""
+ for name, info in sorted_sectors[:10]:
+ # 处理直接获取的数据结构
+ if "top_stock" in info and "top_stock_change" in info:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领涨: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
+ # 处理从数据库获取的数据结构
+ else:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}%")
+
+ text_parts.append(f"""
跌幅榜前10:
""")
- for name, info in sorted_sectors[-10:]:
- text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领跌: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
+ for name, info in sorted_sectors[-10:]:
+ # 处理直接获取的数据结构
+ if "top_stock" in info and "top_stock_change" in info:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领跌: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
+ # 处理从数据库获取的数据结构
+ else:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}%")
# 概念板块表现(前20)
- if data.get("concepts"):
- concepts = data["concepts"]
- sorted_concepts = sorted(concepts.items(), key=lambda x: x[1]["change_pct"], reverse=True)
-
- text_parts.append(f"""
+ if data.get("concepts"):
+ concepts = data["concepts"]
+ sorted_concepts = sorted(concepts.items(), key=lambda x: x[1]["change_pct"], reverse=True)
+
+ text_parts.append(f"""
【概念板块表现 TOP20】
涨幅榜前10:
""")
- for name, info in sorted_concepts[:10]:
- text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领涨: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
+ for name, info in sorted_concepts[:10]:
+ # 处理直接获取的数据结构
+ if "top_stock" in info and "top_stock_change" in info:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}% | 领涨: {info['top_stock']} ({info['top_stock_change']:+.2f}%)")
+ # 处理从数据库获取的数据结构
+ else:
+ text_parts.append(f" {name}: {info['change_pct']:+.2f}%")
# 板块资金流向(前15)
- if data.get("sector_fund_flow") and data["sector_fund_flow"].get("today"):
- flow = data["sector_fund_flow"]["today"]
-
- text_parts.append(f"""
+ if data.get("sector_fund_flow") and data["sector_fund_flow"].get("today"):
+ flow = data["sector_fund_flow"]["today"]
+
+ text_parts.append(f"""
【行业资金流向 TOP15】
主力资金净流入前15:
""")
- sorted_flow = sorted(flow, key=lambda x: x["main_net_inflow"], reverse=True)
- for item in sorted_flow[:15]:
- text_parts.append(f" {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {item['change_pct']:+.2f}%")
+ sorted_flow = sorted(flow, key=lambda x: x["main_net_inflow"], reverse=True)
+ for item in sorted_flow[:15]:
+ # 处理直接获取的数据结构
+ if "change_pct" in item:
+ text_parts.append(f" {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%) | 涨跌: {item['change_pct']:+.2f}%")
+ # 处理从数据库获取的数据结构
+ else:
+ text_parts.append(f" {item['sector']}: {item['main_net_inflow']:.2f}万 ({item['main_net_inflow_pct']:+.2f}%)")
# 重要新闻(前20条)
if data.get("news"):
@@ -642,38 +966,93 @@ def _save_raw_data_to_db(self, data):
def get_cached_data_with_fallback(self):
"""获取缓存数据,支持回退机制"""
- try:
- # 首先尝试获取最新数据
- print("[智策] 尝试获取最新数据...")
- fresh_data = self.get_all_sector_data()
+ # 获取最新数据
+ print("[智策] 尝试获取最新数据...")
+ fresh_data = self.get_all_sector_data()
+
+ # 加载缓存数据
+ print("[智策] 加载缓存数据...")
+ cached_data = self._load_cached_data()
+
+ # 合并新数据和缓存数据
+ merged_data = {
+ "success": False,
+ "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "sectors": {},
+ "concepts": {},
+ "sector_fund_flow": {},
+ "market_overview": {},
+ "north_flow": {},
+ "news": []
+ }
+
+ # 优先使用新数据,缺失的部分使用缓存数据
+ if fresh_data:
+ # 行业板块数据
+ if fresh_data.get("sectors"):
+ merged_data["sectors"] = fresh_data["sectors"]
+ print("[智策] 使用最新行业板块数据")
+ elif cached_data and cached_data.get("sectors"):
+ merged_data["sectors"] = cached_data["sectors"]
+ print("[智策] 使用缓存行业板块数据")
- if fresh_data.get("success"):
- return fresh_data
+ # 概念板块数据
+ if fresh_data.get("concepts"):
+ merged_data["concepts"] = fresh_data["concepts"]
+ print("[智策] 使用最新概念板块数据")
+ elif cached_data and cached_data.get("concepts"):
+ merged_data["concepts"] = cached_data["concepts"]
+ print("[智策] 使用缓存概念板块数据")
- # 如果获取失败,回退到缓存数据
- print("[智策] 获取最新数据失败,尝试加载缓存数据...")
- cached_data = self._load_cached_data()
+ # 资金流向数据
+ if fresh_data.get("sector_fund_flow"):
+ merged_data["sector_fund_flow"] = fresh_data["sector_fund_flow"]
+ print("[智策] 使用最新资金流向数据")
+ elif cached_data and cached_data.get("sector_fund_flow"):
+ merged_data["sector_fund_flow"] = cached_data["sector_fund_flow"]
+ print("[智策] 使用缓存资金流向数据")
- if cached_data:
- print("[智策] ✓ 成功加载缓存数据")
- cached_data["from_cache"] = True
- cached_data["cache_warning"] = "当前显示为缓存数据(24小时内),可能不是最新信息"
- return cached_data
- else:
- print("[智策] ✗ 无可用缓存数据")
- return {
- "success": False,
- "error": "无法获取数据且无可用缓存",
- "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- }
-
- except Exception as e:
- self.logger.error(f"[智策数据] 获取数据失败: {e}")
- return {
- "success": False,
- "error": str(e),
- "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- }
+ # 市场概况数据
+ if fresh_data.get("market_overview"):
+ merged_data["market_overview"] = fresh_data["market_overview"]
+ print("[智策] 使用最新市场概况数据")
+ elif cached_data and cached_data.get("market_overview"):
+ merged_data["market_overview"] = cached_data["market_overview"]
+ print("[智策] 使用缓存市场概况数据")
+
+ # 北向资金数据
+ if fresh_data.get("north_flow"):
+ merged_data["north_flow"] = fresh_data["north_flow"]
+ print("[智策] 使用最新北向资金数据")
+ elif cached_data and cached_data.get("north_flow"):
+ merged_data["north_flow"] = cached_data["north_flow"]
+ print("[智策] 使用缓存北向资金数据")
+
+ # 新闻数据
+ if fresh_data.get("news"):
+ merged_data["news"] = fresh_data["news"]
+ print("[智策] 使用最新新闻数据")
+ elif cached_data and cached_data.get("news"):
+ merged_data["news"] = cached_data["news"]
+ print("[智策] 使用缓存新闻数据")
+
+ # 检查是否有任何数据成功获取
+ has_data = (
+ merged_data["sectors"] or
+ merged_data["concepts"] or
+ merged_data["sector_fund_flow"] or
+ merged_data["market_overview"] or
+ merged_data["north_flow"] or
+ merged_data["news"]
+ )
+
+ merged_data["success"] = bool(has_data)
+
+ if not has_data:
+ print("[智策] ✗ 无可用数据")
+ merged_data["error"] = "无法获取任何数据"
+
+ return merged_data
def _load_cached_data(self):
"""加载缓存数据"""
diff --git a/sector_strategy_engine.py b/sector_strategy_engine.py
index 5e39364..e21b356 100644
--- a/sector_strategy_engine.py
+++ b/sector_strategy_engine.py
@@ -391,6 +391,8 @@ def _generate_final_predictions(self, comprehensive_report: str, agents_results:
2. 分析要基于前期的多维度研判
3. 给出的建议要具体、可操作
4. 预测要客观、理性,避免过度乐观或悲观
+5. 请确保只输出JSON格式内容,不要包含任何其他文字说明
+6. 确保JSON格式完全正确,没有语法错误
"""
messages = [
@@ -402,17 +404,106 @@ def _generate_final_predictions(self, comprehensive_report: str, agents_results:
# 尝试解析JSON
try:
+ # 确保响应是字符串格式
+ if not isinstance(response, str):
+ response = str(response)
+
+ # 首先尝试直接解析整个响应
+ try:
+ predictions = json.loads(response)
+ if isinstance(predictions, dict):
+ print(" ✓ 直接解析JSON成功")
+ return predictions
+ except json.JSONDecodeError:
+ pass
+
+ # 改进的JSON提取逻辑:找到完整的JSON对象
+ # 查找第一个'{'的位置
+ start_pos = response.find('{')
+ if start_pos == -1:
+ print(" ⚠ 未找到JSON开始标记 '{',返回文本格式")
+ self.logger.warning(f"[智策引擎] 未找到JSON开始标记 '{{', 响应内容长度: {len(response)}, 前100字符: {response[:100]}...")
+ return {"prediction_text": response}
+
+ # 从开始位置向后查找完整的JSON结构
+ brace_count = 0
+ end_pos = -1
+ for i in range(start_pos, len(response)):
+ if response[i] == '{':
+ brace_count += 1
+ elif response[i] == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ end_pos = i + 1
+ break
+
+ if end_pos == -1:
+ print(" ⚠ 未找到完整的JSON结构,返回文本格式")
+ self.logger.warning(f"[智策引擎] 未找到完整的JSON结构,响应长度: {len(response)}, 开始位置: {start_pos}, 最后100字符: {response[-100:]}...")
+ return {"prediction_text": response}
+
+ # 提取完整的JSON字符串
+ json_str = response[start_pos:end_pos]
+
+ # 增强的JSON字符串清理
import re
- json_match = re.search(r'\{.*\}', response, re.DOTALL)
- if json_match:
- predictions = json.loads(json_match.group())
- print(" ✓ 预测报告生成成功(JSON格式)")
- return predictions
- else:
- print(" ⚠ 未能解析JSON,返回文本格式")
+
+ # 移除控制字符
+ json_str = re.sub(r'[\u0000-\u001F\u007F-\u009F]', '', json_str)
+
+ # 移除可能的BOM字符
+ if json_str.startswith('\ufeff'):
+ json_str = json_str[1:]
+
+ # 修复可能的编码问题
+ try:
+ json_str = json_str.encode('utf-8').decode('utf-8')
+ except UnicodeEncodeError:
+ # 如果编码失败,使用更安全的处理方式
+ json_str = ''.join([c if ord(c) < 128 else ' ' for c in json_str])
+
+ # 验证JSON格式
+ try:
+ predictions = json.loads(json_str)
+ except json.JSONDecodeError:
+ # 尝试更严格的清理:移除所有非JSON字符
+ json_str = re.sub(r'[^\x20-\x7E\{\}\[\]"\\,:\-0-9.a-zA-Z]', '', json_str)
+ # 再次尝试解析
+ try:
+ predictions = json.loads(json_str)
+ except json.JSONDecodeError as e2:
+ # 记录详细的清理失败信息
+ self.logger.error(f"[智策引擎] JSON清理后仍解析失败: {e2},清理后JSON前100字符: {json_str[:100]}...")
+ raise
+
+ # 检查必要的JSON结构
+ if not isinstance(predictions, dict):
+ print(" ⚠ JSON不是有效的对象格式,返回文本格式")
return {"prediction_text": response}
+
+ print(" ✓ 预测报告生成成功(JSON格式)")
+ return predictions
+ except json.JSONDecodeError as e:
+ # 记录错误位置上下文
+ error_pos = e.pos
+ context = response[max(0, error_pos-50):min(len(response), error_pos+50)]
+ # 计算行号和列号,使用chr(10)避免f-string中的反斜杠错误
+ newline_char = chr(10)
+ line_count = response[:error_pos].count(newline_char) + 1
+ if newline_char in response[:error_pos]:
+ column_count = error_pos - response.rfind(newline_char, 0, error_pos)
+ else:
+ column_count = error_pos + 1
+ line_info = f"行号: {line_count}, 列号: {column_count}"
+ print(f" ⚠ JSON解析失败: {e},{line_info},返回文本格式")
+ self.logger.error(f"[智策引擎] JSON解析失败详情: {e},{line_info},错误位置上下文: {context},完整响应长度: {len(response)}")
+ # 记录完整的响应内容以便调试
+ self.logger.debug(f"[智策引擎] 完整响应内容: {response}")
+ return {"prediction_text": response}
except Exception as e:
- print(f" ⚠ JSON解析失败: {e},返回文本格式")
+ print(f" ⚠ 其他错误: {e},返回文本格式")
+ self.logger.error(f"[智策引擎] 预测生成错误: {type(e).__name__}: {e}")
+ self.logger.debug(f"[智策引擎] 响应内容: {response[:500]}...")
return {"prediction_text": response}
def save_analysis_report(self, results: Dict, original_data: Dict) -> int:
diff --git a/sector_strategy_ui.py b/sector_strategy_ui.py
index dc5f13a..a10c5ad 100644
--- a/sector_strategy_ui.py
+++ b/sector_strategy_ui.py
@@ -133,14 +133,27 @@ def display_analysis_tab():
with col3:
st.write("")
st.write("")
- if st.button("🔄 清除结果", width='content'):
- if 'sector_strategy_result' in st.session_state:
- del st.session_state.sector_strategy_result
- st.success("已清除分析结果")
- st.rerun()
+ # 创建两个子列来放置按钮
+ sub_col1, sub_col2 = st.columns(2)
+ with sub_col1:
+ input_data_button = st.button("📊 输入北向数据", width='content')
+ with sub_col2:
+ if st.button("🔄 清除结果", width='content'):
+ if 'sector_strategy_result' in st.session_state:
+ del st.session_state.sector_strategy_result
+ st.success("已清除分析结果")
+ st.rerun()
st.markdown("---")
+ # 处理输入北向数据按钮
+ if input_data_button:
+ st.session_state.show_north_data_input = True
+
+ # 显示北向数据输入界面
+ if st.session_state.get('show_north_data_input', False):
+ display_north_data_input()
+
# 开始分析
if analyze_button:
# 清除之前的结果
@@ -274,6 +287,12 @@ def run_sector_strategy_analysis(model="deepseek-chat"):
progress_bar.progress(10)
fetcher = SectorStrategyDataFetcher()
+
+ # 检查是否有手动输入的北向资金数据
+ if 'manual_north_data' in st.session_state and st.session_state.manual_north_data is not None:
+ fetcher.set_manual_north_data(st.session_state.manual_north_data)
+ st.info("📝 使用手动输入的北向资金数据进行分析")
+
# 使用带缓存回退的获取逻辑
data = fetcher.get_cached_data_with_fallback()
@@ -342,10 +361,13 @@ def display_data_summary(data):
with col1:
if market.get("sh_index"):
sh = market["sh_index"]
+ # 兼容不同的字段名
+ close_price = sh.get('close', sh.get('最新价', 0))
+ change_pct = sh.get('change_pct', sh.get('涨跌幅', 0))
st.metric(
"上证指数",
- f"{sh['close']:.2f}",
- f"{sh['change_pct']:+.2f}%"
+ f"{close_price:.2f}",
+ f"{change_pct:+.2f}%"
)
with col2:
@@ -1154,6 +1176,180 @@ def test_email_notification():
st.code(traceback.format_exc())
+def display_north_data_input():
+ """显示北向数据输入界面"""
+ st.markdown("### 📊 北向资金数据输入")
+ st.markdown("请在下方表格中输入或上传北向资金数据,数据格式应包含:日期、北向成交总额、沪股通、深股通。数据来源:https://data.eastmoney.com/hsgtV2/hsgtDetail/scgk.html")
+
+ # 创建示例数据结构
+ if 'north_data_input' not in st.session_state:
+ st.session_state.north_data_input = pd.DataFrame({
+ '日期': [''],
+ '北向成交总额': [''],
+ '沪股通': [''],
+ '深股通': ['']
+ })
+
+ # 数据输入区域
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown("**数据输入表格**")
+ # 使用data_editor进行数据编辑
+ edited_data = st.data_editor(
+ st.session_state.north_data_input,
+ num_rows="dynamic",
+ use_container_width=True,
+ key="north_data_editor"
+ )
+
+ # 更新session state
+ st.session_state.north_data_input = edited_data
+
+ with col2:
+ st.markdown("**操作**")
+
+ # 保存数据按钮
+ if st.button("💾 保存数据", type="primary"):
+ try:
+ # 验证数据格式
+ if validate_north_data(edited_data):
+ # 保存到session state
+ st.session_state.manual_north_data = process_north_data(edited_data)
+ st.success("✅ 数据保存成功!")
+ st.info(f"已保存 {len(edited_data)} 条记录")
+ else:
+ st.error("❌ 数据格式验证失败,请检查数据格式")
+ except Exception as e:
+ st.error(f"❌ 保存数据时出错: {str(e)}")
+
+ # 清空数据按钮
+ if st.button("🗑️ 清空数据"):
+ st.session_state.north_data_input = pd.DataFrame({
+ '日期': [],
+ '北向成交总额': [],
+ '沪股通': [],
+ '深股通': []
+ })
+ st.rerun()
+
+ # 关闭输入界面按钮
+ if st.button("❌ 关闭"):
+ st.session_state.show_north_data_input = False
+ st.rerun()
+
+ # 从Excel导入按钮
+ st.markdown("---")
+ uploaded_file = st.file_uploader(
+ "📁 从Excel导入",
+ type=['xlsx', 'xls'],
+ help="上传包含北向资金数据的Excel文件"
+ )
+
+ if uploaded_file is not None:
+ try:
+ # 读取Excel文件
+ df = pd.read_excel(uploaded_file)
+
+ # 处理Excel数据(跳过标题行)
+ if len(df) > 1:
+ # 从第2行开始读取数据(跳过标题行,索引1开始)
+ data_rows = df.iloc[1:]
+
+ # 提取关键列
+ processed_data = pd.DataFrame({
+ '日期': data_rows.iloc[:, 0], # 第1列:日期
+ '北向成交总额': data_rows.iloc[:, 1], # 第2列:北向成交总额
+ '沪股通': data_rows.iloc[:, 2], # 第3列:沪股通
+ '深股通': data_rows.iloc[:, 7] # 第8列:深股通
+ })
+
+ # 过滤有效数据(排除标题行和空值)
+ # 过滤掉日期列为字符串"日期"的行(标题行)
+ valid_mask = (
+ processed_data['日期'].notna() &
+ (processed_data['日期'].astype(str) != '日期') &
+ (processed_data['日期'].astype(str) != 'nan')
+ )
+ processed_data = processed_data[valid_mask]
+
+ if len(processed_data) > 0:
+ st.session_state.north_data_input = processed_data
+ # 自动保存导入的数据
+ if validate_north_data(processed_data):
+ st.session_state.manual_north_data = process_north_data(processed_data)
+ st.success(f"✅ 成功导入并保存 {len(processed_data)} 条记录")
+ st.info("💡 数据已自动保存,可以关闭输入界面开始分析")
+ else:
+ st.success(f"✅ 成功导入 {len(processed_data)} 条记录")
+ st.warning("⚠️ 请点击'保存数据'按钮完成保存")
+ # 数据导入成功后,不需要立即刷新页面,避免无限循环
+ else:
+ st.error("❌ Excel文件中没有找到有效数据")
+ else:
+ st.error("❌ Excel文件数据不足")
+
+ except Exception as e:
+ st.error(f"❌ 导入Excel文件失败: {str(e)}")
+
+ # 显示当前保存的数据状态
+ if 'manual_north_data' in st.session_state:
+ st.markdown("---")
+ st.markdown("**📋 当前已保存的数据**")
+ saved_data = st.session_state.manual_north_data
+ st.info(f"✅ 已保存 {len(saved_data)} 条北向资金数据,可用于智策分析")
+
+ # 显示数据预览
+ with st.expander("查看已保存数据"):
+ st.dataframe(saved_data.head(10), width='stretch')
+
+
+def validate_north_data(data):
+ """验证北向数据格式"""
+ try:
+ if data.empty:
+ return False
+
+ # 检查必要的列
+ required_columns = ['日期', '北向成交总额', '沪股通', '深股通']
+ for col in required_columns:
+ if col not in data.columns:
+ return False
+
+ # 检查是否有有效数据
+ valid_rows = data[data['日期'].notna()]
+ return len(valid_rows) > 0
+
+ except Exception:
+ return False
+
+
+def process_north_data(data):
+ """处理北向数据,转换为标准格式"""
+ try:
+ # 过滤有效数据
+ valid_data = data[data['日期'].notna()].copy()
+
+ # 转换日期格式
+ valid_data['日期'] = pd.to_datetime(valid_data['日期'])
+
+ # 处理金额数据(去除"亿元"等单位)
+ for col in ['北向成交总额', '沪股通', '深股通']:
+ if col in valid_data.columns:
+ valid_data[col] = valid_data[col].astype(str).str.replace('亿元', '').str.replace(',', '')
+ # 转换为数值类型
+ valid_data[col] = pd.to_numeric(valid_data[col], errors='coerce')
+
+ # 按日期排序
+ valid_data = valid_data.sort_values('日期', ascending=False)
+
+ return valid_data
+
+ except Exception as e:
+ st.error(f"处理数据时出错: {str(e)}")
+ return pd.DataFrame()
+
+
# 主入口
if __name__ == "__main__":
display_sector_strategy()
diff --git a/smart_monitor.db b/smart_monitor.db
index 81035a3..374510d 100644
Binary files a/smart_monitor.db and b/smart_monitor.db differ
diff --git a/smart_monitor_engine.py b/smart_monitor_engine.py
index 0c35798..a13beac 100644
--- a/smart_monitor_engine.py
+++ b/smart_monitor_engine.py
@@ -60,6 +60,7 @@ def __init__(self, deepseek_api_key: str = None, qmt_account_id: str = None,
self.qmt.connect(qmt_account_id or "simulator")
self.logger.info("使用模拟交易模式")
else:
+ self.logger.info("尝试连接miniQMT...")
self.qmt = SmartMonitorQMT()
if qmt_account_id:
success = self.qmt.connect(qmt_account_id)
@@ -209,7 +210,10 @@ def analyze_stock(self, stock_code: str, auto_trade: bool = False,
stock_name=market_data.get('name'),
decision=decision,
execution_result=execution_result,
- market_data=market_data
+ market_data=market_data,
+ has_position=has_position,
+ position_cost=position_cost,
+ session_info=session_info
)
return {
@@ -409,7 +413,8 @@ def _execute_sell(self, stock_code: str, decision: Dict, market_data: Dict) -> D
def _send_notification(self, stock_code: str, stock_name: str,
decision: Dict, execution_result: Optional[Dict],
- market_data: Dict):
+ market_data: Dict, has_position: bool = False,
+ position_cost: float = 0, session_info: Optional[Dict] = None):
"""
发送通知(使用主程序的通知服务)
优化策略:仅在买入或卖出信号时发送通知,持有信号不发送
@@ -494,7 +499,7 @@ def _send_notification(self, stock_code: str, stock_name: str,
'position_cost': f"{position_cost:.2f}" if has_position and position_cost else 'N/A',
'profit_loss_pct': f"{((market_data.get('current_price', 0) - position_cost) / position_cost * 100):+.2f}" if has_position and position_cost else 'N/A',
# 交易时段信息
- 'trading_session': session_info.get('session', '未知')
+ 'trading_session': session_info.get('session', '未知') if session_info else '未知'
}
# 直接调用主程序的通知服务发送
diff --git a/smart_monitor_qmt.py b/smart_monitor_qmt.py
index 9cb1521..3edbace 100644
--- a/smart_monitor_qmt.py
+++ b/smart_monitor_qmt.py
@@ -6,9 +6,15 @@
import logging
import os
+import sys
from typing import Dict, List, Optional
from datetime import datetime
+# 确保可以导入config_manager
+project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if project_root not in sys.path:
+ sys.path.insert(0, project_root)
+
class SmartMonitorQMT:
"""miniQMT交易接口"""
@@ -32,15 +38,18 @@ def __init__(self, mini_qmt_path: str = None):
self.xtdata = xtdata
self.logger.info("miniQMT模块加载成功")
except ImportError as e:
- self.logger.warning(f"miniQMT模块未安装: {e}")
- self.logger.warning("将使用模拟模式(不实际下单)")
+ self.logger.error(f"miniQMT模块未安装: {e}")
+ self.logger.error("将使用模拟模式(不实际下单)")
+ except Exception as e:
+ self.logger.error(f"加载miniQMT模块失败: {e}")
+ self.logger.error("将使用模拟模式(不实际下单)")
def connect(self, account_id: str = None) -> bool:
"""
连接miniQMT
Args:
- account_id: 交易账户ID(可选,从环境变量读取)
+ account_id: 交易账户ID(可选,从环境配置读取)
Returns:
是否连接成功
@@ -50,25 +59,64 @@ def connect(self, account_id: str = None) -> bool:
self.connected = False
return False
- # 从配置读取账户ID
- if account_id is None:
- account_id = os.getenv('MINIQMT_ACCOUNT_ID', '')
-
- if not account_id:
- self.logger.error("未配置miniQMT账户ID,请在环境配置中设置")
- self.connected = False
- return False
-
try:
- # 创建交易对象
- self.xt_trader = self.xttrader.XtQuantTrader()
+ config = {}
+ try:
+ from config_manager import config_manager
+ config = config_manager.read_env()
+ except Exception as e:
+ self.logger.warning(f"无法读取配置文件,将使用环境变量: {e}")
+
+ account_id = account_id or config.get('MINIQMT_ACCOUNT_ID', '') or os.getenv('MINIQMT_ACCOUNT_ID', '')
+
+ if not account_id:
+ self.logger.error("未配置miniQMT账户ID,请在环境配置中设置")
+ self.connected = False
+ return False
+
+ mini_qmt_path = config.get('MINIQMT_PATH', '') or os.getenv('MINIQMT_PATH', '')
+ if not mini_qmt_path:
+ self.logger.error("未配置miniQMT路径,请在环境配置中设置")
+ self.connected = False
+ return False
+
+ if mini_qmt_path.endswith('userdata_mini'):
+ full_path = mini_qmt_path
+ else:
+ full_path = os.path.join(mini_qmt_path, 'userdata_mini')
+
+ if not os.path.exists(full_path):
+ self.logger.error(f"MiniQMT路径不存在: {full_path}")
+ self.logger.error(f"请检查配置中的MINIQMT_PATH是否正确设置")
+ self.logger.error(f"正确的路径应该指向MiniQMT的userdata_mini文件夹")
+ return False
+
+ import uuid
+ session_id = int(str(uuid.getnode())[:8])
+
+ self.logger.info(f"连接miniQMT,路径: {full_path}, 账户: {account_id}, 会话ID: {session_id}")
+
+ # 首先尝试停止可能存在的旧连接
+ if hasattr(self, 'xt_trader') and self.xt_trader:
+ try:
+ self.xt_trader.stop()
+ self.logger.info("已停止旧的miniQMT连接")
+ except:
+ pass # 忽略停止旧连接的错误
+
+ self.xt_trader = self.xttrader.XtQuantTrader(path=full_path, session=session_id)
- # 连接
self.xt_trader.start()
- # 连接账户
- self.account = self.xttrader.StockAccount(account_id)
+ from xtquant.xttype import StockAccount
+ self.account = StockAccount(account_id)
+
+ # 添加延迟确保客户端完全启动
+ import time
+ time.sleep(2) # 等待2秒确保客户端启动
+
connect_result = self.xt_trader.connect()
+
if connect_result == 0:
self.connected = True
@@ -76,10 +124,27 @@ def connect(self, account_id: str = None) -> bool:
return True
else:
self.logger.error(f"miniQMT连接失败,错误码: {connect_result}")
+ self.logger.error("请检查以下问题:")
+ self.logger.error("1. MiniQMT客户端是否已启动并完全加载")
+ self.logger.error("2. MINIQMT_PATH路径是否正确")
+ self.logger.error("3. MINIQMT_ACCOUNT_ID是否正确")
+ self.logger.error("4. MiniQMT客户端是否正常运行且未被其他程序占用")
+ self.logger.error("5. 确保同一时间只有一个Python程序连接到MiniQMT")
+
+ # 尝试连接其他账户以测试连接性
+ try:
+ test_account = StockAccount(account_id + "_test") # 创建测试账户
+ test_result = self.xt_trader.connect()
+ self.logger.info(f"连接测试结果: {test_result}")
+ except:
+ pass
+
return False
except Exception as e:
self.logger.error(f"连接miniQMT失败: {e}")
+ import traceback
+ self.logger.error(f"详细错误: {traceback.format_exc()}")
return False
def disconnect(self):
diff --git a/startsytem.bat b/startsytem.bat
new file mode 100644
index 0000000..2c0c29c
--- /dev/null
+++ b/startsytem.bat
@@ -0,0 +1,28 @@
+@echo off
+chcp 65001 >nul
+setlocal
+
+set VENV_PATH=%~dp0TradEnv
+set PYTHON_EXE="%VENV_PATH%\python.exe"
+
+if not exist %PYTHON_EXE% (
+ echo [错误] 未找到Python解释器: %PYTHON_EXE%
+ echo 请确保TradEnv目录存在
+ pause
+ exit /b 1
+)
+
+echo [INFO] 启动智能盯盘系统...
+echo [INFO] 工作目录: %cd%
+echo [INFO] Python路径: %PYTHON_EXE%
+
+%PYTHON_EXE% -m streamlit run "%~dp0app.py"
+
+if errorlevel 1 (
+ echo [错误] 系统启动失败
+ pause
+ exit /b 1
+)
+
+endlocal
+pause
\ No newline at end of file
diff --git a/stock_analysis.db b/stock_analysis.db
index 6dfc294..145d2bc 100644
Binary files a/stock_analysis.db and b/stock_analysis.db differ
diff --git a/stock_monitor.db b/stock_monitor.db
index 7d84a01..57549ed 100644
Binary files a/stock_monitor.db and b/stock_monitor.db differ
diff --git a/test_tdx_api.py b/test_tdx_api.py
deleted file mode 100644
index c777723..0000000
--- a/test_tdx_api.py
+++ /dev/null
@@ -1,174 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-TDX API配置测试脚本
-用于测试TDX API连接和数据获取是否正常
-"""
-
-import os
-import sys
-import requests
-from dotenv import load_dotenv
-
-# 加载环境变量
-load_dotenv()
-
-# 获取TDX API URL
-TDX_API_URL = os.getenv('TDX_BASE_URL', 'http://127.0.0.1:5000')
-
-print("=" * 60)
-print("TDX API配置测试")
-print("=" * 60)
-print(f"\n1. TDX API地址: {TDX_API_URL}")
-
-# 测试1: 健康检查
-print("\n2. 测试健康检查接口...")
-try:
- response = requests.get(f"{TDX_API_URL}/api/health", timeout=5)
- if response.status_code == 200:
- print(" ✅ 健康检查成功")
- print(f" 响应: {response.text}")
- else:
- print(f" ❌ 健康检查失败: HTTP {response.status_code}")
- sys.exit(1)
-except Exception as e:
- print(f" ❌ 连接失败: {e}")
- print("\n提示:")
- print(" - 请检查TDX API服务是否已启动")
- print(" - 请检查.env中的TDX_API_URL配置是否正确")
- print(" - 默认地址: http://192.168.1.222:8181")
- sys.exit(1)
-
-# 测试2: 获取K线数据
-print("\n3. 测试K线数据接口...")
-
-# 尝试不同的代码格式
-test_codes = [
- ("SZ000001", "平安银行"),
- ("000001", "平安银行(纯数字)"),
- ("SH600000", "浦发银行"),
- ("600000", "浦发银行(纯数字)"),
-]
-
-data = None
-test_code = None
-
-for code, name in test_codes:
- print(f"\n 尝试股票: {code} ({name})")
-
- try:
- url = f"{TDX_API_URL}/api/kline"
- params = {
- 'code': code,
- 'type': 'day'
- }
-
- response = requests.get(url, params=params, timeout=10)
-
- if response.status_code == 200:
- data = response.json()
-
- # 支持两种数据格式
- kline_list = None
- if isinstance(data, dict) and 'data' in data:
- # 嵌套格式: {code: 0, message: "success", data: {List: [...]}}
- if data.get('code') == 0:
- data_obj = data.get('data', {})
- kline_list = data_obj.get('List', [])
- elif isinstance(data, list):
- # 直接数组格式
- kline_list = data
-
- if kline_list and len(kline_list) > 0:
- test_code = code
- data = kline_list # 保存为全局变量
- print(f" ✅ K线数据获取成功!")
- print(f" 数据条数: {len(kline_list)}")
- break
- else:
- print(f" ⚠️ 数据为空")
- else:
- print(f" ❌ HTTP {response.status_code}")
- except Exception as e:
- print(f" ❌ 错误: {e}")
-
-if data is None or test_code is None:
- print(f"\n ❌ 所有代码格式都失败,无法继续测试")
- print("\n提示:")
- print(" - 请检查TDX API服务是否正确启动")
- print(" - 请确认API支持的股票代码格式")
- print(" - 可能的格式:SZ000001, 000001, SH600000, 600000")
- sys.exit(1)
-
-print(f"\n 成功的代码格式: {test_code}")
-
-# 显示最新一条数据
-if len(data) > 0:
- latest = data[-1]
- print(f"\n 最新K线数据:")
- # 支持两种字段名格式:小写和大写
- print(f" - 日期: {latest.get('date') or latest.get('Time', 'N/A')}")
- print(f" - 开盘: {latest.get('open') or latest.get('Open', 'N/A')}")
- print(f" - 收盘: {latest.get('close') or latest.get('Close', 'N/A')}")
- print(f" - 最高: {latest.get('high') or latest.get('High', 'N/A')}")
- print(f" - 最低: {latest.get('low') or latest.get('Low', 'N/A')}")
- print(f" - 成交量: {latest.get('volume') or latest.get('Volume', 'N/A')}")
-
-# 检查数据量是否足够计算MA20
-if len(data) >= 20:
- print(f" ✅ 数据量充足,可以计算MA20(需要至少20条)")
-else:
- print(f" ⚠️ 数据量不足,仅{len(data)}条,需要至少20条才能计算MA20")
- print(f" 请尝试其他股票或等待数据积累")
-
-# 测试3: 计算均线
-print("\n4. 测试均线计算...")
-try:
- import pandas as pd
-
- df = pd.DataFrame(data)
-
- # 支持两种字段名:小写close和大写Close
- if 'Close' in df.columns and 'close' not in df.columns:
- df['close'] = df['Close']
-
- df['close'] = pd.to_numeric(df['close'], errors='coerce')
-
- # 计算MA5和MA20
- df['MA5'] = df['close'].rolling(window=5).mean()
- df['MA20'] = df['close'].rolling(window=20).mean()
-
- latest = df.iloc[-1]
-
- if pd.notna(latest['MA5']) and pd.notna(latest['MA20']):
- print(f" ✅ 均线计算成功")
- print(f" - 收盘价: {latest['close']:.2f}")
- print(f" - MA5: {latest['MA5']:.2f}")
- print(f" - MA20: {latest['MA20']:.2f}")
-
- # 判断MA5和MA20的关系
- if latest['MA5'] > latest['MA20']:
- print(f" - 趋势: 🟢 MA5 > MA20 (多头)")
- elif latest['MA5'] < latest['MA20']:
- print(f" - 趋势: 🔴 MA5 < MA20 (空头)")
- else:
- print(f" - 趋势: 🟡 MA5 = MA20 (震荡)")
- else:
- print(f" ❌ 均线计算失败,数据包含NaN")
- sys.exit(1)
-
-except Exception as e:
- print(f" ❌ 均线计算失败: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-# 所有测试通过
-print("\n" + "=" * 60)
-print("✅ 所有测试通过!TDX API配置正常")
-print("=" * 60)
-print("\n提示:")
-print(" - 现在可以启动低价擒牛策略监控服务")
-print(" - 在监控面板中点击'▶️ 启动监控服务'")
-print(" - 服务将每60秒扫描一次监控列表中的股票")
-print("")
diff --git "a/\345\220\257\345\212\250\347\263\273\347\273\237.bat" "b/\345\220\257\345\212\250\347\263\273\347\273\237.bat"
index 810824a..bf71de3 100644
--- "a/\345\220\257\345\212\250\347\263\273\347\273\237.bat"
+++ "b/\345\220\257\345\212\250\347\263\273\347\273\237.bat"
@@ -1,7 +1,57 @@
@echo off
-set VENV_PATH=.\TradEnv
-set PYTHON_EXE="%VENV_PATH%\python.exe"
-set STREAMLIT_MODULE="streamlit.cli"
-cd /d ..\AIagentsStock
-%PYTHON_EXE% -m streamlit run app.py
+chcp 65001 >nul
+setlocal
+
+echo ============================================
+echo 智能盯盘系统 - 一键启动
+echo ============================================
+echo.
+
+set SERVER_EXE=d:\AI\MICC\tdx-api\web\server.exe
+set MINI_QMT_EXE=d:\AI\MICC\SMT-Q\bin.x64\XtItClient.exe
+set START_BAT=d:\AI\MICC\AIagentsStock\startsytem.bat
+
+echo [步骤 1/3] 启动TDX服务器...
+if exist "%SERVER_EXE%" (
+ start "TDX Server" "%SERVER_EXE%"
+ echo [OK] TDX服务器已启动
+) else (
+ echo [警告] 未找到 TDX 服务器: %SERVER_EXE%
+)
+echo 等待 3 秒...
+ping -n 4 127.0.0.1 >nul
+echo.
+
+echo [步骤 2/3] 启动 MiniQMT 客户端...
+if exist "%MINI_QMT_EXE%" (
+ start "MiniQMT" "%MINI_QMT_EXE%"
+ echo [OK] MiniQMT 客户端已启动
+) else (
+ echo [警告] 未找到 MiniQMT 客户端: %MINI_QMT_EXE%
+)
+echo 等待 3 秒...
+ping -n 4 127.0.0.1 >nul
+echo.
+
+echo [步骤 3/3] 启动智能盯盘系统...
+if exist "%START_BAT%" (
+ start "Smart Monitor" cmd /c "%START_BAT%"
+ echo [OK] 智能盯盘系统已启动
+) else (
+ echo [错误] 未找到启动脚本: %START_BAT%
+)
+echo.
+
+echo ============================================
+echo 所有服务已启动完成!
+echo ============================================
+echo.
+echo 请在浏览器中访问: http://localhost:8501
+echo.
+echo 注意:
+echo - MiniQMT 需要登录您的交易账户
+echo - 确保 TDX 服务器正常运行
+echo - 查看浏览器中的系统状态
+echo.
+endlocal
pause
\ No newline at end of file