From be76e444c7ea684eef3595feebc5a81642f5cd80 Mon Sep 17 00:00:00 2001 From: oficcejo Date: Sun, 14 Dec 2025 12:31:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=89=E8=82=A1=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ++ app.py | 17 +- profit_growth_monitor.db | Bin 0 -> 20480 bytes profit_growth_monitor.py | 307 +++++++++++++++++++++++++++ profit_growth_selector.py | 130 ++++++++++++ profit_growth_ui.py | 425 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 899 insertions(+), 2 deletions(-) create mode 100644 profit_growth_monitor.db create mode 100644 profit_growth_monitor.py create mode 100644 profit_growth_selector.py create mode 100644 profit_growth_ui.py diff --git a/README.md b/README.md index ba9678c..756dc13 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,28 @@ - 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎! +## ⭐ 1214更新 - 净利增长策略 📈 + +新增“净利增长策略”选股板块,专注稳健成长股票: + +**核心功能:** +- ✅ **稳健成长** - 净利润增长率≥10%,稳健可持续 +- ✅ **成交优先** - 按成交额由小到大排序,选择低流动性股票 +- ✅ **区域聚焦** - 仅限深圳A股,排除科创板和创业板 +- ✅ **策略监控** - 自动跟踪持仓,智能提醒卖出 +- ✅ **仓位管理** - 资金5万、个股最大4成、单日最多买1股 +- ✅ **铉钉通知** - 选股结果和卖出信号自动通知 + +**使用流程:** +1. 在选股板块中点击“📈 净利增长” +2. 执行选股,查看筛选结果 +3. 点击“➕ 加入策略监控”加入监控 +4. 在策略监控面板查看卖出提醒 + +> **注意**:当前监控服务暂时使用MA5下穿MA20作为卖出信号,后续将升级支持KDJ指标。 + +--- + ## ⭐ 1214更新 - 小市值策略 📊 新增“小市值策略”选股板块,专注小盘高成长股票: diff --git a/app.py b/app.py index f21e56c..f030097 100644 --- a/app.py +++ b/app.py @@ -314,14 +314,21 @@ def main(): 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_longhubang', 'show_portfolio', 'show_main_force', 'show_small_cap', 'show_profit_growth']: if key in st.session_state: del st.session_state[key] 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_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_profit_growth']: + if key in st.session_state: + del st.session_state[key] + + 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: del st.session_state[key] @@ -486,6 +493,12 @@ def main(): from small_cap_ui import display_small_cap display_small_cap() return + + # 检查是否显示净利增长策略 + if 'show_profit_growth' in st.session_state and st.session_state.show_profit_growth: + from profit_growth_ui import display_profit_growth + display_profit_growth() + return # 检查是否显示智策板块 if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy: diff --git a/profit_growth_monitor.db b/profit_growth_monitor.db new file mode 100644 index 0000000000000000000000000000000000000000..193bb9e0a81c94f5e647296c5e7dda00cb406e6d GIT binary patch literal 20480 zcmeI%O>f#T7zgl#LOUU$_Qusx4+RybP5S^_wq_G5q%BRfahNPO_O_I~w24!ddk>ZN zd8U1}9p?lXFHH+Q4*e|!Z2ZKse|{3l;{2@ZNkP6valj;bqwQ(BuDv5f)3lPhD(ae+ ztX-Zq)VDrYf2>;4Uf%qy?f%iq)n8ie^X~6jMO9EB009U<00Izz00bcL9D%!Dd8fX= zuirDtE_{&)->1wMu}tQwl@EQ>uuWnc@4F_M>mZFPDJUK%9m_U9nSFBF@AQoRfE=3x zVmS6m$5LbUOv`Q+-kHeA9a1;qg4pI4n^-66a=P88qC*y}6`e1O%F)#`qGQ35D9q+l zq0(WtsYm8V!|B@O)q_23o^EZ|4-fUhIGe70bt0mPxK2f=##n5Y7qe=yvyts;OWoPL z`>OBM+im?m$j1z#(34RtIL&h Tuple[bool, str]: + """ + 添加股票到监控列表 + + Args: + stock_code: 股票代码 + stock_name: 股票名称 + buy_price: 买入价格 + buy_date: 买入日期(可选,默认为当前日期) + + Returns: + (是否成功, 消息) + """ + try: + if buy_date is None: + buy_date = datetime.now().strftime("%Y-%m-%d") + + add_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 检查是否已存在 + cursor.execute(""" + SELECT id FROM monitored_stocks + WHERE stock_code = ? AND status = 'holding' + """, (stock_code,)) + + if cursor.fetchone(): + conn.close() + return False, f"股票 {stock_code} 已在监控列表中" + + # 插入新记录 + cursor.execute(""" + INSERT INTO monitored_stocks + (stock_code, stock_name, buy_price, buy_date, add_time) + VALUES (?, ?, ?, ?, ?) + """, (stock_code, stock_name, buy_price, buy_date, add_time)) + + conn.commit() + conn.close() + + self.logger.info(f"添加股票到监控: {stock_code} {stock_name}") + return True, f"成功添加 {stock_name} 到监控列表" + + except Exception as e: + self.logger.error(f"添加股票失败: {e}") + return False, f"添加失败: {str(e)}" + + def get_monitoring_stocks(self) -> List[Dict]: + """获取所有监控中的股票""" + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT * FROM monitored_stocks + WHERE status = 'holding' + ORDER BY add_time DESC + """) + + stocks = [dict(row) for row in cursor.fetchall()] + conn.close() + + return stocks + + except Exception as e: + self.logger.error(f"获取监控股票失败: {e}") + return [] + + def update_holding_days(self, stock_code: str, days: int) -> bool: + """更新持股天数""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + UPDATE monitored_stocks + SET holding_days = ? + WHERE stock_code = ? AND status = 'holding' + """, (days, stock_code)) + + conn.commit() + conn.close() + return True + + except Exception as e: + self.logger.error(f"更新持股天数失败: {e}") + return False + + def add_sell_alert(self, stock_code: str, stock_name: str, alert_type: str, + alert_reason: str = None, current_price: float = None, + kdj_k: float = None, kdj_d: float = None, kdj_j: float = None, + holding_days: int = None) -> Tuple[bool, str]: + """添加卖出提醒""" + try: + alert_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO sell_alerts + (stock_code, stock_name, alert_type, alert_reason, + current_price, kdj_k, kdj_d, kdj_j, holding_days, alert_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (stock_code, stock_name, alert_type, alert_reason, + current_price, kdj_k, kdj_d, kdj_j, holding_days, alert_time)) + + conn.commit() + conn.close() + + self.logger.info(f"添加卖出提醒: {stock_code} - {alert_type}") + return True, "提醒添加成功" + + except Exception as e: + self.logger.error(f"添加卖出提醒失败: {e}") + return False, str(e) + + def get_unprocessed_alerts(self) -> List[Dict]: + """获取未处理的卖出提醒""" + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT * FROM sell_alerts + WHERE is_processed = 0 + ORDER BY alert_time DESC + """) + + alerts = [dict(row) for row in cursor.fetchall()] + conn.close() + + return alerts + + except Exception as e: + self.logger.error(f"获取卖出提醒失败: {e}") + return [] + + def get_all_alerts(self, limit: int = 50) -> List[Dict]: + """获取所有卖出提醒历史""" + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT * FROM sell_alerts + ORDER BY alert_time DESC + LIMIT ? + """, (limit,)) + + alerts = [dict(row) for row in cursor.fetchall()] + conn.close() + + return alerts + + except Exception as e: + self.logger.error(f"获取提醒历史失败: {e}") + return [] + + def remove_stock(self, stock_code: str, reason: str = "手动移除") -> Tuple[bool, str]: + """从监控列表移除股票""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 先检查是否存在持仓中的股票 + cursor.execute(""" + SELECT id FROM monitored_stocks + WHERE stock_code = ? AND status = 'holding' + """, (stock_code,)) + + if not cursor.fetchone(): + conn.close() + return False, f"股票 {stock_code} 不在监控列表中" + + # 先删除该股票的所有'removed'记录(避免UNIQUE约束冲突) + cursor.execute(""" + DELETE FROM monitored_stocks + WHERE stock_code = ? AND status = 'removed' + """, (stock_code,)) + + # 然后更新持仓中的记录为'removed' + cursor.execute(""" + UPDATE monitored_stocks + SET status = 'removed', + remove_time = ?, + remove_reason = ? + WHERE stock_code = ? AND status = 'holding' + """, (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), reason, stock_code)) + + conn.commit() + conn.close() + + self.logger.info(f"移除股票: {stock_code}, 原因: {reason}") + return True, f"成功移除 {stock_code}" + except Exception as e: + self.logger.error(f"移除股票失败: {e}") + return False, f"移除失败: {str(e)}" + + def get_removed_stocks(self, limit: int = 50) -> List[Dict]: + """获取已移除的股票历史""" + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT * FROM monitored_stocks + WHERE status = 'removed' + ORDER BY remove_time DESC + LIMIT ? + """, (limit,)) + + stocks = [dict(row) for row in cursor.fetchall()] + conn.close() + + return stocks + + except Exception as e: + self.logger.error(f"获取移除历史失败: {e}") + return [] + + +# 全局实例 +profit_growth_monitor = ProfitGrowthMonitor() diff --git a/profit_growth_selector.py b/profit_growth_selector.py new file mode 100644 index 0000000..530f313 --- /dev/null +++ b/profit_growth_selector.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +净利增长选股模块 +使用pywencai进行股票筛选 +""" + +import logging +from typing import Tuple, Optional +import pandas as pd + + +class ProfitGrowthSelector: + """净利增长选股器""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + + def get_profit_growth_stocks(self, top_n: int = 5) -> Tuple[bool, Optional[pd.DataFrame], str]: + """ + 获取符合净利增长策略的股票 + + 筛选条件: + - 净利润增长率 ≥ 10%(净利润同比增长率) + - 深圳A股 + - 非科创板 + - 非创业板 + - 非ST + - 按成交额由小到大排名 + + Args: + top_n: 返回前N只股票 + + Returns: + (是否成功, 数据DataFrame, 消息) + """ + try: + import pywencai + + # 构建查询语句(按成交额由小至大排名) + query = ( + "净利润增长率(净利润同比增长率)≥10%," + "非科创板," + "非创业板," + "非ST," + "深圳A股," + "成交额由小至大排名" + ) + + self.logger.info(f"开始执行净利增长选股,查询条件: {query}") + + # 调用pywencai + result = pywencai.get(query=query, loop=True) + + if result is None or result.empty: + self.logger.warning("未获取到符合条件的股票") + return False, None, "未找到符合条件的股票" + + self.logger.info(f"获取到 {len(result)} 只股票") + + # 取前N只 + if len(result) > top_n: + result = result.head(top_n) + self.logger.info(f"筛选前 {top_n} 只股票") + + return True, result, f"成功获取 {len(result)} 只股票" + + except ImportError: + error_msg = "pywencai模块未安装,请执行: pip install pywencai" + self.logger.error(error_msg) + return False, None, error_msg + + except Exception as e: + error_msg = f"选股失败: {str(e)}" + self.logger.error(error_msg) + import traceback + traceback.print_exc() + return False, None, error_msg + + def format_stock_info(self, df: pd.DataFrame) -> str: + """ + 格式化股票信息为文本 + + Args: + df: 股票数据DataFrame + + Returns: + 格式化后的文本 + """ + if df is None or df.empty: + return "无数据" + + lines = [] + for idx, row in df.iterrows(): + stock_code = row.get('股票代码', 'N/A') + stock_name = row.get('股票简称', 'N/A') + profit_growth = row.get('净利润增长率', row.get('净利润同比增长率', 'N/A')) + turnover = row.get('成交额', row.get('成交额[20241213]', 'N/A')) + + line = f"{idx+1}. {stock_code} {stock_name}" + + # 添加详细信息 + details = [] + + if profit_growth != 'N/A': + try: + details.append(f"净利增长:{float(profit_growth):.2f}%") + except: + pass + + if turnover != 'N/A': + try: + turnover_val = float(turnover) + if turnover_val >= 100000000: + details.append(f"成交额:{turnover_val/100000000:.2f}亿") + else: + details.append(f"成交额:{turnover_val/10000:.2f}万") + except: + pass + + if details: + line += f" - {', '.join(details)}" + + lines.append(line) + + return '\n'.join(lines) + + +# 全局实例 +profit_growth_selector = ProfitGrowthSelector() diff --git a/profit_growth_ui.py b/profit_growth_ui.py new file mode 100644 index 0000000..75ee03c --- /dev/null +++ b/profit_growth_ui.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +净利增长策略UI模块 +""" + +import streamlit as st +import pandas as pd +from datetime import datetime +from typing import List, Dict +from profit_growth_selector import profit_growth_selector +from notification_service import notification_service +from profit_growth_monitor import profit_growth_monitor + + +def display_profit_growth(): + """显示净利增长策略界面""" + + # 检查是否显示监控面板 + if st.session_state.get('show_profit_growth_monitor'): + display_profit_growth_monitor_panel() + + # 返回按钮 + if st.button("🔙 返回选股", type="secondary"): + del st.session_state.show_profit_growth_monitor + st.rerun() + return + + st.markdown("### 顶部按钮区") + col_select, col_monitor = st.columns([3, 1]) + + with col_select: + st.markdown("## 📈 净利增长策略 - 稳健成长股票筛选") + + with col_monitor: + st.write("") # 占位 + if st.button("📊 策略监控", type="primary", use_container_width=True): + st.session_state.show_profit_growth_monitor = True + st.rerun() + + st.markdown("---") + + st.markdown(""" + ### 📋 选股策略说明 + + **筛选条件**: + - ✅ 净利润增长率 ≥ 10%(净利润同比增长率) + - ✅ 深圳A股 + - ✅ 非ST股票 + - ✅ 非创业板 + - ✅ 非科创板 + - ✅ 按成交额由小到大排名 + + **量化交易策略**: + - 💰 资金量:5万元 + - 📅 持股周期:5天 + - 💼 仓位控制:满仓 + - 📊 个股最大持仓:4成(40%) + - 🎯 账户最大持股数:4只 + - 🛒 单日最大买入数:1只 + - 📈 买入时机:开盘买入 + - 📉 卖出时机:KDJ死叉或持股满5天 + + > **注意**:当前监控服务暂时使用MA5下穿MA20作为卖出信号,后续将升级支持KDJ指标。 + """) + + st.markdown("---") + + # 参数设置 + col1, col2 = st.columns([2, 1]) + + with col1: + top_n = st.slider( + "筛选数量", + min_value=3, + max_value=10, + value=5, + step=1, + help="选择展示的股票数量" + ) + + with col2: + st.info(f"💡 将筛选成交额最小的前{top_n}只股票") + + st.markdown("---") + + # 开始选股按钮 + if st.button("🚀 开始净利增长选股", type="primary", use_container_width=True): + + with st.spinner("正在获取数据,请稍候..."): + # 创建选股器 + success, stocks_df, message = profit_growth_selector.get_profit_growth_stocks(top_n) + + if not success: + st.error(f"❌ {message}") + return + + st.success(f"✅ {message}") + + # 保存到session_state + st.session_state.profit_growth_stocks = stocks_df + st.session_state.profit_growth_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 显示选股结果 + if 'profit_growth_stocks' in st.session_state and st.session_state.profit_growth_stocks is not None: + st.markdown("---") + st.markdown("## 📈 选股结果") + + stocks_df = st.session_state.profit_growth_stocks + select_time = st.session_state.profit_growth_time + + st.info(f"🕒 选股时间:{select_time} | 📊 股票数量:{len(stocks_df)} 只") + + # 显示股票列表 + display_stock_list(stocks_df) + + # 发送钉钉通知 + st.markdown("---") + if st.button("📲 发送钉钉通知", type="secondary", use_container_width=True): + send_dingtalk_notification(stocks_df) + + +def display_stock_list(stocks_df: pd.DataFrame): + """显示股票列表""" + + for idx, row in stocks_df.iterrows(): + stock_code = row.get('股票代码', 'N/A') + stock_name = row.get('股票简称', 'N/A') + + with st.expander(f"📊 {idx+1}. {stock_code} {stock_name}", expanded=True): + display_stock_detail(row) + + +def display_stock_detail(row: pd.Series): + """显示股票详细信息""" + + # 获取所有可能的字段 + financial_fields = [ + ('净利润增长率', row.get('净利润增长率', row.get('净利润同比增长率', None))), + ('成交额', row.get('成交额', row.get('成交额[20241213]', None))), + ('股价', row.get('股价', row.get('最新价', None))), + ('市盈率', row.get('市盈率', row.get('市盈率TTM', None))), + ('市净率', row.get('市净率', row.get('市净率PB', None))), + ('所属行业', row.get('所属行业', row.get('所属同花顺行业', None))), + ] + + # 检查是否有任何有效数据 + has_any_data = any(is_valid_value(value) for _, value in financial_fields) + + # 决定布局 + if has_any_data: + col1, col2 = st.columns(2) + else: + col1 = st.container() + col2 = None + + with col1: + st.markdown("#### 📊 基本信息") + st.markdown(f"**股票代码**: {row.get('股票代码', 'N/A')}") + st.markdown(f"**股票名称**: {row.get('股票简称', 'N/A')}") + + # 只有当有财务数据时才显示财务指标 + if col2 is not None: + with col2: + st.markdown("#### 💼 财务指标") + + for field_name, value in financial_fields: + if is_valid_value(value): + formatted_value = format_value(value, get_suffix(field_name)) + st.markdown(f"**{field_name}**: {formatted_value}") + + # 添加监控按钮 + st.markdown("---") + st.markdown("#### 📊 策略监控") + + stock_code = row.get('股票代码', '') + stock_name = row.get('股票简称', '') + price = row.get('股价', row.get('最新价', None)) + + # 去掉代码后缀 + if isinstance(stock_code, str) and '.' in stock_code: + stock_code = stock_code.split('.')[0] + + # 转换价格 + try: + price_float = float(price) if price and not pd.isna(price) else None + except: + price_float = None + + if stock_code and stock_name: + add_stock_to_monitor_button(stock_code, stock_name, price_float) + + +def add_stock_to_monitor_button(stock_code: str, stock_name: str, price: float = None): + """添加股票到监控的按钮""" + + button_key = f"add_monitor_{stock_code}" + + if st.button(f"➕ 加入策略监控", key=button_key, use_container_width=True): + + # 获取价格 + if price is None: + st.warning("⚠️ 无法获取股票价格,请手动输入") + return + + # 添加到监控 + success, message = profit_growth_monitor.add_stock( + stock_code=stock_code, + stock_name=stock_name, + buy_price=price + ) + + if success: + st.success(f"✅ {message}") + else: + st.error(f"❌ {message}") + + +def display_profit_growth_monitor_panel(): + """显示净利增长监控面板""" + + st.markdown("## 📊 净利增长策略监控") + st.markdown("---") + + # 获取监控中的股票 + monitoring_stocks = profit_growth_monitor.get_monitoring_stocks() + + # 标签页 + tab1, tab2, tab3 = st.tabs(["📊 监控列表", "⚠️ 卖出提醒", "📜 历史记录"]) + + with tab1: + display_monitoring_list(monitoring_stocks) + + with tab2: + display_sell_alerts() + + with tab3: + display_history() + + +def display_monitoring_list(stocks: List[Dict]): + """显示监控列表""" + + st.markdown("### 📊 持仓监控") + + if not stocks: + st.info("📭 监控列表为空,请先添加股票") + return + + st.info(f"📊 当前监控 {len(stocks)} 只股票") + + for stock in stocks: + with st.expander(f"📊 {stock['stock_code']} {stock['stock_name']}", expanded=False): + col1, col2, col3 = st.columns([2, 2, 1]) + + with col1: + st.markdown(f"**买入价格**: {stock['buy_price']:.2f}元") + st.markdown(f"**买入日期**: {stock['buy_date']}") + + with col2: + st.markdown(f"**持股天数**: {stock['holding_days']}天") + st.markdown(f"**加入时间**: {stock['add_time']}") + + with col3: + if st.button("🗑️ 移除", key=f"remove_{stock['stock_code']}", use_container_width=True): + success, msg = profit_growth_monitor.remove_stock(stock['stock_code'], "手动移除") + if success: + st.success(msg) + st.rerun() + else: + st.error(msg) + + +def display_sell_alerts(): + """显示卖出提醒""" + + st.markdown("### ⚠️ 卖出提醒") + + alerts = profit_growth_monitor.get_unprocessed_alerts() + + if not alerts: + st.info("📭 暂无新的卖出提醒") + return + + st.warning(f"⚠️ 有 {len(alerts)} 条待处理提醒") + + for alert in alerts: + with st.container(): + st.markdown("---") + col1, col2 = st.columns([3, 1]) + + with col1: + st.markdown(f"### 📊 {alert['stock_code']} {alert['stock_name']}") + st.markdown(f"**提醒类型**: {alert['alert_type']}") + st.markdown(f"**提醒原因**: {alert['alert_reason']}") + st.markdown(f"**提醒时间**: {alert['alert_time']}") + + with col2: + st.markdown("#### 💰 市场数据") + + current_price = alert.get('current_price') + if current_price is not None: + try: + price_val = float(current_price) + st.markdown(f"**当前价格**: {price_val:.2f}元") + except (ValueError, TypeError): + st.markdown(f"**当前价格**: {current_price}") + + holding_days = alert.get('holding_days') + if holding_days is not None: + st.markdown(f"**持有天数**: {holding_days}天") + + +def display_history(): + """显示历史记录""" + + st.markdown("### 📜 历史记录") + + # 子标签 + sub_tab1, sub_tab2 = st.tabs(["⚠️ 提醒历史", "🗑️ 移除历史"]) + + with sub_tab1: + alerts = profit_growth_monitor.get_all_alerts(50) + if alerts: + st.info(f"📊 共 {len(alerts)} 条提醒记录") + for alert in alerts: + st.markdown(f"- **{alert['alert_time']}** | {alert['stock_code']} {alert['stock_name']} | {alert['alert_type']}") + else: + st.info("📭 暂无提醒历史") + + with sub_tab2: + removed = profit_growth_monitor.get_removed_stocks(50) + if removed: + st.info(f"📊 共 {len(removed)} 条移除记录") + for stock in removed: + st.markdown(f"- **{stock['remove_time']}** | {stock['stock_code']} {stock['stock_name']} | {stock['remove_reason']}") + else: + st.info("📭 暂无移除历史") + + +def is_valid_value(value): + """判断值是否有效""" + if value is None: + return False + if pd.isna(value): + return False + if str(value).strip() in ['', 'N/A', 'nan', 'None']: + return False + return True + + +def format_value(value, suffix=''): + """格式化显示值""" + if isinstance(value, (int, float)): + if abs(value) >= 100000000: # 亿 + return f"{value/100000000:.2f}亿{suffix}" + elif abs(value) >= 10000: # 万 + return f"{value/10000:.2f}万{suffix}" + else: + return f"{value:.2f}{suffix}" + return f"{value}{suffix}" + + +def get_suffix(field_name: str) -> str: + """获取字段后缀""" + suffix_map = { + '净利润增长率': '%', + '成交额': '元', + '股价': '元', + } + return suffix_map.get(field_name, '') + + +def send_dingtalk_notification(stocks_df: pd.DataFrame): + """发送钉钉通知""" + + try: + if not notification_service.config['webhook_enabled']: + st.warning("⚠️ Webhook通知未启用,请在系统配置中启用") + return + + # 构建消息 + keyword = notification_service.config.get('webhook_keyword', 'aiagents通知') + + message_text = f"### {keyword} - 净利增长选股完成\n\n" + message_text += "**筛选策略**: 净利润增长率≥10% + 深圳A股 + 非ST/创业/科创\n\n" + message_text += f"**筛选数量**: {len(stocks_df)} 只\n\n" + message_text += "**精选股票**:\n\n" + + for idx, row in stocks_df.iterrows(): + stock_code = row.get('股票代码', 'N/A') + stock_name = row.get('股票简称', 'N/A') + message_text += f"{idx+1}. {stock_code} {stock_name}\n\n" + + message_text += f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" + message_text += "_此消息由AI股票分析系统自动发送_" + + # 直接发送钉钉Webhook + if notification_service.config['webhook_type'] == 'dingtalk': + import requests + + data = { + "msgtype": "markdown", + "markdown": { + "title": f"{keyword}", + "text": message_text + } + } + + response = requests.post( + notification_service.config['webhook_url'], + json=data, + headers={'Content-Type': 'application/json'}, + timeout=10 + ) + + if response.status_code == 200: + st.success("✅ 钉钉通知发送成功") + else: + st.error(f"❌ 钉钉通知发送失败: HTTP {response.status_code}") + else: + st.warning("⚠️ 当前仅支持钉钉通知") + + except Exception as e: + st.error(f"❌ 发送通知失败: {str(e)}")