增加选股策略
This commit is contained in:
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
- 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎!
|
- 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎!
|
||||||
|
|
||||||
|
## ⭐ 1214更新 - 净利增长策略 📈
|
||||||
|
|
||||||
|
新增“净利增长策略”选股板块,专注稳健成长股票:
|
||||||
|
|
||||||
|
**核心功能:**
|
||||||
|
- ✅ **稳健成长** - 净利润增长率≥10%,稳健可持续
|
||||||
|
- ✅ **成交优先** - 按成交额由小到大排序,选择低流动性股票
|
||||||
|
- ✅ **区域聚焦** - 仅限深圳A股,排除科创板和创业板
|
||||||
|
- ✅ **策略监控** - 自动跟踪持仓,智能提醒卖出
|
||||||
|
- ✅ **仓位管理** - 资金5万、个股最大4成、单日最多买1股
|
||||||
|
- ✅ **铉钉通知** - 选股结果和卖出信号自动通知
|
||||||
|
|
||||||
|
**使用流程:**
|
||||||
|
1. 在选股板块中点击“📈 净利增长”
|
||||||
|
2. 执行选股,查看筛选结果
|
||||||
|
3. 点击“➕ 加入策略监控”加入监控
|
||||||
|
4. 在策略监控面板查看卖出提醒
|
||||||
|
|
||||||
|
> **注意**:当前监控服务暂时使用MA5下穿MA20作为卖出信号,后续将升级支持KDJ指标。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⭐ 1214更新 - 小市值策略 📊
|
## ⭐ 1214更新 - 小市值策略 📊
|
||||||
|
|
||||||
新增“小市值策略”选股板块,专注小盘高成长股票:
|
新增“小市值策略”选股板块,专注小盘高成长股票:
|
||||||
|
|||||||
@@ -314,14 +314,21 @@ def main():
|
|||||||
if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"):
|
if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"):
|
||||||
st.session_state.show_low_price_bull = True
|
st.session_state.show_low_price_bull = True
|
||||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
if st.button("📊 小市值策略", width='stretch', key="nav_small_cap", help="小盘高成长股票筛选策略"):
|
if st.button("📊 小市值策略", width='stretch', key="nav_small_cap", help="小盘高成长股票筛选策略"):
|
||||||
st.session_state.show_small_cap = True
|
st.session_state.show_small_cap = True
|
||||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
@@ -486,6 +493,12 @@ def main():
|
|||||||
from small_cap_ui import display_small_cap
|
from small_cap_ui import display_small_cap
|
||||||
display_small_cap()
|
display_small_cap()
|
||||||
return
|
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:
|
if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy:
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,307 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
净利增长策略监控数据库管理模块
|
||||||
|
复用低价擒牛的数据库结构,但标识为不同策略
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class ProfitGrowthMonitor:
|
||||||
|
"""净利增长策略监控数据库管理"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = "profit_growth_monitor.db"):
|
||||||
|
"""
|
||||||
|
初始化监控数据库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: 数据库文件路径
|
||||||
|
"""
|
||||||
|
self.db_path = db_path
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self._init_database()
|
||||||
|
|
||||||
|
def _init_database(self):
|
||||||
|
"""初始化数据库表结构"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建监控股票表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS monitored_stocks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
stock_code TEXT NOT NULL,
|
||||||
|
stock_name TEXT NOT NULL,
|
||||||
|
buy_price REAL NOT NULL,
|
||||||
|
buy_date TEXT NOT NULL,
|
||||||
|
holding_days INTEGER DEFAULT 0,
|
||||||
|
status TEXT DEFAULT 'holding',
|
||||||
|
add_time TEXT NOT NULL,
|
||||||
|
remove_time TEXT,
|
||||||
|
remove_reason TEXT,
|
||||||
|
UNIQUE(stock_code, status)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建卖出提醒表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS sell_alerts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
stock_code TEXT NOT NULL,
|
||||||
|
stock_name TEXT NOT NULL,
|
||||||
|
alert_type TEXT NOT NULL,
|
||||||
|
alert_reason TEXT,
|
||||||
|
current_price REAL,
|
||||||
|
kdj_k REAL,
|
||||||
|
kdj_d REAL,
|
||||||
|
kdj_j REAL,
|
||||||
|
holding_days INTEGER,
|
||||||
|
alert_time TEXT NOT NULL,
|
||||||
|
is_processed INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
self.logger.info("净利增长监控数据库初始化成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"数据库初始化失败: {e}")
|
||||||
|
|
||||||
|
def add_stock(self, stock_code: str, stock_name: str, buy_price: float,
|
||||||
|
buy_date: str = None) -> 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()
|
||||||
@@ -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()
|
||||||
@@ -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)}")
|
||||||
Reference in New Issue
Block a user