This commit is contained in:
oficcejo
2026-02-27 20:06:45 +08:00
parent 63628ffdd5
commit 9ee27de0ed
6 changed files with 918 additions and 2 deletions
+420
View File
@@ -0,0 +1,420 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低估值策略UI模块
"""
import streamlit as st
import pandas as pd
from datetime import datetime
from value_stock_selector import ValueStockSelector
from value_stock_strategy import ValueStockStrategy
def display_value_stock():
"""显示低估值选股界面"""
st.markdown("""
<div style="background: linear-gradient(135deg, #1a5276 0%, #2e86c1 50%, #1a5276 100%);
padding: 2rem; border-radius: 15px; margin-bottom: 1.5rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
<h1 style="color: #fff; margin: 0; font-size: 2rem;">
💎 低估值策略 - 价值投资选股
</h1>
<p style="color: rgba(255,255,255,0.7); margin: 0.5rem 0 0 0; font-size: 0.9rem;">
基于视频 <a href="https://www.bilibili.com/video/BV1eJfxBrEjZ" target="_blank" style="color: #7ec8e3; text-decoration: underline;">头号投资法则</a>
</p>
<p style="color: rgba(255,255,255,0.8); margin: 0.3rem 0 0 0; font-size: 1.1rem;">
低PE + 低PB + 高股息 + 低负债 — 寻找被市场低估的优质标的
</p>
</div>
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown("""
### 📋 选股策略说明
**筛选条件**
- ✅ 市盈率(PE)≤ 20
- ✅ 市净率(PB)≤ 1.5
- ✅ 股息率 ≥ 1%
- ✅ 资产负债率 ≤ 30%
- ✅ 非ST股票
- ✅ 非科创板
- ✅ 非创业板
- ✅ 按流通市值由小到大排名
**量化交易策略**
- 💰 资金量:100万元
- 📈 买入时机:开盘买入
- 💼 单股最大仓位:30%
- 🎯 最大持股数:4只
- 🛒 每日最多买入:2只
- 📉 卖出条件①:持股满30天到期卖出
- 📉 卖出条件②:RSI超买(>70)卖出
""")
st.markdown("---")
# 参数设置
col1, col2 = st.columns([2, 1])
with col1:
top_n = st.slider(
"筛选数量",
min_value=5,
max_value=20,
value=10,
step=1,
help="选择展示的股票数量",
key="value_stock_top_n"
)
with col2:
st.info(f"💡 将筛选流通市值最小的前{top_n}只低估值股票")
st.markdown("---")
# 开始选股按钮
if st.button("🚀 开始低估值选股", type="primary", width='content', key="value_stock_start"):
with st.spinner("正在获取数据,请稍候..."):
selector = ValueStockSelector()
success, stocks_df, message = selector.get_value_stocks(top_n=top_n)
if success and stocks_df is not None:
st.session_state.value_stocks = stocks_df
st.session_state.value_stock_selector = selector
st.success(f"{message}")
st.rerun()
else:
st.error(f"{message}")
# 显示选股结果
if 'value_stocks' in st.session_state:
display_stock_results(
st.session_state.value_stocks,
st.session_state.get('value_stock_selector')
)
def display_stock_results(stocks_df: pd.DataFrame, selector):
"""显示选股结果"""
st.markdown("---")
st.markdown("## 📊 选股结果")
# 统计信息
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("筛选数量", f"{len(stocks_df)}")
with col2:
pe_col = None
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
pe_col = matching[0]
break
if pe_col:
valid = pd.to_numeric(stocks_df[pe_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PE", f"{valid.mean():.1f}")
else:
st.metric("平均PE", "-")
else:
st.metric("平均PE", "-")
with col3:
pb_col = None
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
pb_col = matching[0]
valid = pd.to_numeric(stocks_df[pb_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PB", f"{valid.mean():.2f}")
else:
st.metric("平均PB", "-")
else:
st.metric("平均PB", "-")
with col4:
div_col = None
matching = [col for col in stocks_df.columns if '股息率' in col]
if matching:
div_col = matching[0]
valid = pd.to_numeric(stocks_df[div_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均股息率", f"{valid.mean():.2f}%")
else:
st.metric("平均股息率", "-")
else:
st.metric("平均股息率", "-")
st.markdown("---")
# 显示股票列表
st.markdown("### 📋 精选低估值股票")
for idx, row in stocks_df.iterrows():
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
# 获取关键指标用于标题
pe_val = ''
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pe_val = f" PE:{float(v):.1f}"
except:
pass
break
pb_val = ''
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pb_val = f" PB:{float(v):.2f}"
except:
pass
with st.expander(
f"【第{idx+1}名】{code} - {name}{pe_val}{pb_val}",
expanded=(idx < 3)
):
display_stock_detail(row, stocks_df)
# 完整数据表格
st.markdown("---")
st.markdown("### 📊 完整数据表格")
# 选择关键列
display_cols = ['股票代码', '股票简称']
for pattern in ['最新价', '股价']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for pattern in ['市盈率', '市净率', '股息率', '资产负债率', '流通市值', '所属行业']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
final_cols = [col for col in display_cols if col in stocks_df.columns]
if final_cols:
st.dataframe(stocks_df[final_cols], width='content', height=400)
csv = stocks_df[final_cols].to_csv(index=False, encoding='utf-8-sig')
st.download_button(
label="📥 下载股票列表CSV",
data=csv,
file_name=f"value_stock_{datetime.now().strftime('%Y%m%d')}.csv",
mime="text/csv",
key="value_csv_download"
)
# 量化交易模拟
st.markdown("---")
display_strategy_simulation(stocks_df, selector)
def display_stock_detail(row: pd.Series, df: pd.DataFrame):
"""显示单个股票详情"""
def is_valid(value):
if value is None:
return False
if isinstance(value, float) and pd.isna(value):
return False
if isinstance(value, str) and value.strip() in ('', 'N/A', 'nan', 'None'):
return False
return True
def fmt(value, suffix=''):
if not is_valid(value):
return "-"
try:
return f"{float(value):.2f}{suffix}"
except:
return str(value) + suffix
# 基本估值数据
col1, col2, col3, col4 = st.columns(4)
with col1:
for p in ['市盈率', '市盈率(动态)']:
m = [c for c in df.columns if p in c]
if m:
st.metric("📊 市盈率(PE)", fmt(row.get(m[0])))
break
with col2:
m = [c for c in df.columns if '市净率' in c]
if m:
st.metric("📊 市净率(PB)", fmt(row.get(m[0])))
with col3:
m = [c for c in df.columns if '股息率' in c]
if m:
st.metric("💰 股息率", fmt(row.get(m[0]), '%'))
with col4:
m = [c for c in df.columns if '资产负债率' in c]
if m:
st.metric("📉 资产负债率", fmt(row.get(m[0]), '%'))
# 补充信息
st.markdown("**其他指标**")
info_parts = []
for pattern in ['最新价', '股价', '流通市值', '总市值', '所属行业', '涨跌幅']:
m = [c for c in df.columns if pattern in c]
if m:
val = row.get(m[0])
if is_valid(val):
info_parts.append(f"**{pattern}**: {val}")
if info_parts:
st.markdown(" | ".join(info_parts))
def display_strategy_simulation(stocks_df: pd.DataFrame, selector):
"""显示量化交易策略模拟"""
st.markdown("## 🎯 策略模拟")
st.info("""
**策略规则**
- 📈 **买入**:开盘价买入,单股最大仓位30%,每日最多买2只
- 📉 **卖出条件①**:持股满30天,到期自动卖出
- 📉 **卖出条件②**RSI(14) > 70 超买,触发卖出
- 🎯 **最大持股**4只
- 💰 **初始资金**:100万元
""")
col1, col2 = st.columns(2)
with col1:
if st.button("🎮 开始策略模拟", type="primary", width='content', key="value_sim_start"):
st.session_state.show_value_strategy_sim = True
with col2:
pass
if st.session_state.get('show_value_strategy_sim'):
run_strategy_simulation(stocks_df)
def run_strategy_simulation(stocks_df: pd.DataFrame):
"""运行策略模拟"""
st.markdown("---")
st.markdown("### 📈 策略模拟执行")
strategy = ValueStockStrategy(initial_capital=1000000.0)
# 模拟买入
st.markdown("#### 1️⃣ 模拟买入信号")
buy_results = []
current_date = datetime.now().strftime("%Y-%m-%d")
for idx, row in stocks_df.head(strategy.max_daily_buy).iterrows():
code = str(row.get('股票代码', '')).split('.')[0]
name = row.get('股票简称', 'N/A')
# 尝试获取价格
price = 0
for p in ['最新价', '股价']:
m = [c for c in stocks_df.columns if p in c]
if m:
try:
price = float(row.get(m[0], 0))
except:
pass
if price > 0:
break
if price > 0:
success, message, trade = strategy.buy(code, name, price, current_date)
buy_results.append({
'success': success,
'message': message,
'trade': trade
})
for result in buy_results:
if result['success']:
st.success(result['message'])
else:
st.warning(f"⚠️ {result['message']}")
# RSI检查
st.markdown("---")
st.markdown("#### 2️⃣ RSI卖出信号检测")
with st.spinner("正在计算RSI指标..."):
for code, pos in list(strategy.positions.items()):
rsi = strategy.calculate_rsi(code)
if rsi is not None:
if rsi > strategy.rsi_overbought:
st.warning(f"⚠️ {code} {pos['name']} RSI={rsi} > {strategy.rsi_overbought},触发超买卖出信号!")
else:
st.info(f"{code} {pos['name']} RSI={rsi},正常范围")
else:
st.info(f"{code} {pos['name']} RSI计算中...")
# 显示持仓
st.markdown("---")
st.markdown("#### 3️⃣ 当前持仓")
positions = strategy.get_positions()
if positions:
positions_df = pd.DataFrame(positions)
st.dataframe(positions_df, width='content')
else:
st.info("暂无持仓")
# 显示账户摘要
st.markdown("---")
st.markdown("#### 4️⃣ 账户摘要")
summary = strategy.get_portfolio_summary()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("初始资金", f"{summary['initial_capital']:,.0f}")
with col2:
st.metric("可用资金", f"{summary['available_cash']:,.0f}")
with col3:
st.metric("持仓市值", f"{summary['position_value']:,.0f}")
with col4:
st.metric("总资产", f"{summary['total_assets']:,.0f}")
st.markdown("---")
st.markdown("#### 📝 策略说明")
st.markdown("""
**后续操作**
1. **持有期管理**:系统跟踪每只股票的持有天数(30天到期)
2. **RSI监测**:每日收盘后计算RSI(14)
- RSI > 70:超买信号,提示卖出
- RSI < 30:超卖信号(可作为加仓参考)
3. **轮动买入**:卖出后释放资金,继续买入新的低估值股票
**风险提示**
- ⚠️ 本策略为模拟演示,实际交易存在滑点、手续费等成本
- ⚠️ 低估值不代表没有风险,价值陷阱需警惕
- ⚠️ 请谨慎评估风险,理性投资
""")
# 主入口
if __name__ == "__main__":
display_value_stock()