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