我的任务:(UI 设计 + 布局)学习与任务指南!!!!!!!!
一、需要学习的内容(超简单,1-2 小时就能上手)
(一)Streamlit 基础操作(核心工具)
安装与启动
- 学什么:怎么让 Streamlit 跑起来,看到自己的界面
- 怎么做:
- 打开命令行(Windows 按
Win+R
输入cmd
;Mac 用 Launchpad 找 “终端” ) - 输入
pip install streamlit
(装 Streamlit 库,只需输 1 次) - 输入
streamlit run app.py
(启动看板,app.py
是你的代码文件名) - 浏览器自动弹出看板页面,就成功啦!
- 打开命令行(Windows 按
基础语法(复制即用)
- 学什么:怎么用几行代码加标题、按钮、下拉框
- 怎么做:
- 标题:
st.title("我是大标题")
- 下拉框:
selected = st.selectbox("选一个", ["选项1", "选项2"])
- 按钮:
if st.button("点我"): st.write("你点啦!")
- 直接把这些代码贴到
app.py
里,运行看效果,改文字就能用!
- 标题:
(二)简单界面美化(不用学 CSS !)
改标题颜色 / 字体
- 学什么:让标题更醒目,不用懂复杂样式代码
- 怎么做:复制这段代码到
app.py
,改颜色值(比如#FF9933
换成你喜欢的色号)python
运行
st.markdown( "<h1 style='color: #FF9933; font-family: 黑体;'>ETF 看板</h1>", unsafe_allow_html=True )
改按钮样式
- 学什么:让按钮变好看,适配金融主题
- 怎么做:复制这段代码,改颜色(
#1E90FF
是深蓝色,可换成#FF5733
橙色)python
运行
st.markdown( "<style>div.stButton > button {background-color: #1E90FF; color: white; font-size: 16px;}</style>", unsafe_allow_html=True )
(三)手绘原型图(用 WPS 就能做)
- 学什么:怎么快速画出界面布局,让队友清楚你的设计
- 怎么做:
- 打开 WPS 演示(或 PowerPoint )→ 新建空白页
- 用 “矩形”“文本框” 拼出界面:
- 画个大矩形当 “顶部标题栏”,写 “ETF 数据看板”
- 左边画小矩形当 “ETF 选择区”,放下拉框 + 按钮
- 右边分上下,画折线图(用 “曲线” 模拟)和表格(用 “直线” 拼)
- 保存为
界面原型草图.jpg
,发给队友!
二、具体任务拆解(按天做,超清晰)
Day3:界面基础美化 + 手绘原型
上午:让看板 “好看起来”(2 小时)
启动 Streamlit:
- 找到团队共享的
app.py
(或自己新建一个,复制基础代码 ) - 命令行输入
streamlit run app.py
,看到默认界面
- 找到团队共享的
加页面配置:
- 打开
app.py
,开头加这段代码(复制):python
运行
import streamlit as st st.set_page_config( page_title="ETF 看板", page_icon="📊", layout="wide" # 宽屏,内容不挤 )
- 重启 Streamlit,看浏览器标签和界面变宽!
- 打开
美化标题和按钮:
- 在
app.py
里加标题美化代码(改颜色 / 字体) - 加按钮美化代码(改按钮颜色)
- 保存后,刷新页面看变化,调整到满意
- 在
下午:手绘原型图(2 小时)
- 打开 WPS 演示,新建空白页
- 画界面布局:
- 顶部:大标题 “ETF 数据可视化看板”
- 左侧:下拉框(选 ETF )+ 查询按钮
- 右侧上:日线行情折线图(用曲线画)
- 右侧下:分钟行情预览表格(用直线拼)
- 保存为
界面原型草图.jpg
,发给队友(成员 D、组长)
Day5:多选项卡功能扩展
上午:给看板加 “选项卡”(2 小时)
复制选项卡代码:
- 打开
app.py
,找到原来的内容,替换成这段(复制):python
运行
import streamlit as st st.set_page_config(...) # 之前的页面配置 # 创建 3 个选项卡 tab1, tab2, tab3 = st.tabs(["基本信息", "行情分析", "指标"]) with tab1: st.subheader("ETF 基本信息") # 原来的“基本信息查询”代码贴这里 with tab2: st.subheader("行情走势") # 原来的“日线行情、分钟行情”代码贴这里 with tab3: st.subheader("金融指标") # 留空,等成员 D 填指标代码
- 打开
调整内容排版:
- 在每个选项卡内的图表 / 下拉框前,加
st.markdown("<br>", unsafe_allow_html=True)
(加空行,让内容不挤) - 给图表加标题,比如
st.subheader("日线行情(近 30 天)")
- 在每个选项卡内的图表 / 下拉框前,加
验证效果:
- 重启 Streamlit,切换选项卡,看内容是否正常显示
- 截图发给队友,同步布局调整后的样子
下午:适配与优化(2 小时)
根据队友反馈调整:
- 如果成员 D 说 “指标模块放不下”,就在选项卡 3 里加
st.expander("点击看指标")
做折叠 - 如果界面太丑,再改改标题颜色、按钮大小
- 如果成员 D 说 “指标模块放不下”,就在选项卡 3 里加
更新原型图:
- 打开 WPS ,在原来的原型图上,标记 “选项卡 1/2/3” 对应的功能
- 保存为
界面原型草图_更新版.jpg
,发群里
三、遇到问题怎么办?(超全解决方案)
Streamlit 启动报错:
- 问题:命令行显示
找不到 app.py
- 解决:先
cd
到代码所在文件夹(比如cd D:\ETF项目
),再运行streamlit run app.py
- 问题:命令行显示
美化代码不生效:
- 问题:改了颜色,页面没变化
- 解决:检查代码里有没有
unsafe_allow_html=True
,重启 Streamlit(ctrl+C
终止,再重新运行 )
手绘原型太丑:
- 解决:用 WPS 演示的 “模板”→ 选 “商务汇报” 类,直接改文字和布局,更简单!
四、你要输出的成果(超有成就感)
Day3 结束:
- 美化后的 Streamlit 看板(有自定义标题、按钮样式,宽屏布局 )
界面原型草图.jpg
(手绘 / WPS 做的界面布局图 )
Day5 结束:
- 带多选项卡的 Streamlit 看板(功能分区清晰,选项卡切换正常 )
界面原型草图_更新版.jpg
(标记选项卡功能,辅助队友开发 )
跟着这份指南,你不用懂复杂编程,就能做出专业感的 UI 设计!全程复制代码、改文字、手绘布局,轻松完成任务,还能和队友完美配合~
(UI 设计 + 布局)完整代码
以下是基于 Streamlit 的 ETF 数据可视化看板完整代码,聚焦 UI 设计与布局优化,包含选项卡功能、界面美化和交互设计。代码已添加详细注释,可直接复制运行:
python
运行
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体支持
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
sns.set(font='SimHei')
# 页面基础配置(成员C负责的UI优化部分)
st.set_page_config(
page_title="ETF数据可视化看板",
page_icon="📊",
layout="wide", # 宽屏布局
initial_sidebar_state="expanded" # 侧边栏默认展开
)
# 顶部标题美化(成员C设计的视觉风格)
st.markdown(
"""
<style>
.title {
text-align: center;
color: #1E40AF; /* 深蓝色主题 */
font-family: '微软雅黑', sans-serif;
font-size: 32px;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #6B7280;
font-size: 18px;
margin-bottom: 30px;
}
.sidebar-title {
color: #1E40AF;
font-weight: bold;
font-size: 20px;
}
.stButton>button {
background-color: #1E40AF;
color: white;
border-radius: 5px;
font-size: 16px;
padding: 8px 16px;
}
.stButton>button:hover {
background-color: #1D4ED8;
}
.stSelectbox select {
font-size: 16px;
}
</style>
""",
unsafe_allow_html=True
)
st.markdown("<h1 class='title'>ETF数据可视化看板</h1>", unsafe_allow_html=True)
st.markdown("<p class='subtitle'>专业的ETF基础数据查询与分析工具</p>", unsafe_allow_html=True)
# 侧边栏设计(成员C优化的导航区域)
with st.sidebar:
st.markdown("<h2 class='sidebar-title'>功能导航</h2>", unsafe_allow_html=True)
menu_option = st.radio(
"选择功能模块",
["基本信息查询", "行情走势分析", "金融指标计算", "多ETF对比"]
)
st.markdown("---")
st.markdown("### 查询设置")
days_range = st.slider("日线数据范围(天数)", min_value=10, max_value=120, value=30)
st.markdown("---")
st.markdown("### 关于")
st.info("""
ETF数据可视化分析系统
版本:v1.0
团队成员C设计
""")
# 模拟数据加载(成员C无需关心数据来源,直接使用)
@st.cache_data
def load_mock_data():
"""生成模拟数据用于UI演示"""
# ETF基本信息
basic_data = {
'ts_code': ['510300.SH', '510500.SH', '159915.SZ', '510050.SH', '512880.SH'],
'name': ['沪深300ETF', '中证500ETF', '创业板ETF', '上证50ETF', '证券ETF'],
'tracking_idx': ['000300.SH', '000905.SH', '399006.SZ', '000016.SH', '399975.SZ'],
'list_date': ['20120504', '20130206', '20110920', '20041230', '20161202'],
'management': ['华夏基金', '南方基金', '易方达基金', '华夏基金', '国泰基金']
}
basic = pd.DataFrame(basic_data)
# 基准指数信息
index_data = {
'ts_code': ['000300.SH', '000905.SH', '399006.SZ', '000016.SH', '399975.SZ'],
'name': ['沪深300', '中证500', '创业板指', '上证50', '证券公司'],
'base_date': ['20041231', '20041231', '20100531', '20031231', '20130715'],
'base_point': [1000, 1000, 1000, 1000, 1000],
'publisher': ['中证指数', '中证指数', '深圳证券交易所', '中证指数', '深圳证券交易所']
}
index = pd.DataFrame(index_data)
# 日线行情数据
today = datetime.now()
dates = [(today - timedelta(days=i)).strftime('%Y%m%d') for i in range(120, 0, -1)]
daily_data = []
for etf_code in basic['ts_code']:
# 模拟价格走势(不同ETF不同趋势)
if etf_code == '510300.SH':
close = 4.5 + np.random.normal(0, 0.1, 120).cumsum()
elif etf_code == '510500.SH':
close = 6.0 + np.random.normal(0, 0.15, 120).cumsum()
elif etf_code == '159915.SZ':
close = 2.8 + np.random.normal(0, 0.2, 120).cumsum()
elif etf_code == '510050.SH':
close = 2.7 + np.random.normal(0, 0.12, 120).cumsum()
else:
close = 1.2 + np.random.normal(0, 0.18, 120).cumsum()
for i, date in enumerate(dates):
daily_data.append({
'ts_code': etf_code,
'trade_date': date,
'open': close[i] * 0.995 + np.random.normal(0, 0.01),
'high': close[i] * 1.01 + np.random.normal(0, 0.01),
'low': close[i] * 0.99 + np.random.normal(0, 0.01),
'close': close[i],
'vol': int(np.random.normal(10000000, 3000000))
})
daily = pd.DataFrame(daily_data)
# 复权因子数据
adj_data = []
for etf_code in basic['ts_code']:
factor = 1.0 + np.random.normal(0, 0.01, 120).cumsum() / 10
for i, date in enumerate(dates):
adj_data.append({
'ts_code': etf_code,
'trade_date': date,
'adj_factor': factor[i]
})
adj = pd.DataFrame(adj_data)
return basic, index, daily, adj
# 加载模拟数据
basic, index, daily, adj = load_mock_data()
# 主页面内容(成员C设计的选项卡布局)
if menu_option == "基本信息查询":
col1, col2 = st.columns([1, 2])
with col1:
st.subheader("ETF 选择")
selected_etf = st.selectbox(
"请选择ETF",
basic['name'].tolist(),
index=0
)
etf_code = basic[basic['name'] == selected_etf]['ts_code'].values[0]
tracking_idx = basic[basic['name'] == selected_etf]['tracking_idx'].values[0]
st.markdown("---")
st.subheader("ETF 代码")
st.info(f"{etf_code}")
st.subheader("跟踪指数")
st.info(f"{index[index['ts_code'] == tracking_idx]['name'].values[0]}")
st.markdown("---")
st.button("刷新数据")
with col2:
st.subheader(f"{selected_etf} 基本信息")
# 显示基本信息表格(美化版)
etf_info = basic[basic['name'] == selected_etf].copy()
etf_info = etf_info.rename(columns={
'ts_code': '代码',
'name': '名称',
'tracking_idx': '跟踪指数',
'list_date': '上市日期',
'management': '管理公司'
})
st.dataframe(etf_info.style.set_properties(**{
'background-color': '#F8FAFC',
'border': '1px solid #E2E8F0',
'padding': '8px',
'text-align': 'center'
}))
st.markdown("---")
# 显示关联指数信息
st.subheader(f"{index[index['ts_code'] == tracking_idx]['name'].values[0]} 指数信息")
idx_info = index[index['ts_code'] == tracking_idx].copy()
idx_info = idx_info.rename(columns={
'ts_code': '指数代码',
'name': '指数名称',
'base_date': '基日',
'base_point': '基点',
'publisher': '发布方'
})
st.dataframe(idx_info.style.set_properties(**{
'background-color': '#F8FAFC',
'border': '1px solid #E2E8F0',
'padding': '8px',
'text-align': 'center'
}))
elif menu_option == "行情走势分析":
col1, col2 = st.columns([1, 3])
with col1:
st.subheader("ETF 选择")
selected_etf = st.selectbox(
"请选择ETF查看行情",
basic['name'].tolist(),
index=0
)
etf_code = basic[basic['name'] == selected_etf]['ts_code'].values[0]
st.markdown("---")
st.subheader("数据范围")
date_range = st.slider(
"选择日期范围",
min_value=datetime.strptime(daily['trade_date'].min(), '%Y%m%d'),
max_value=datetime.strptime(daily['trade_date'].max(), '%Y%m%d'),
value=(datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=30),
datetime.strptime(daily['trade_date'].max(), '%Y%m%d')),
format="YYYY-MM-DD"
)
start_date = date_range[0].strftime('%Y%m%d')
end_date = date_range[1].strftime('%Y%m%d')
st.markdown("---")
st.subheader("图表设置")
show_volume = st.checkbox("显示成交量", value=True)
show_ma = st.checkbox("显示均线", value=True)
ma_days = st.slider("均线周期", min_value=5, max_value=60, value=20)
st.markdown("---")
st.button("刷新图表")
with col2:
# 筛选数据
etf_daily = daily[(daily['ts_code'] == etf_code) &
(daily['trade_date'] >= start_date) &
(daily['trade_date'] <= end_date)].copy()
# 转换日期格式
etf_daily['trade_date'] = pd.to_datetime(etf_daily['trade_date'], format='%Y%m%d')
# 计算均线
if show_ma:
etf_daily[f'MA{ma_days}'] = etf_daily['close'].rolling(window=ma_days).mean()
# 创建图表
fig, ax1 = plt.subplots(figsize=(12, 6))
# 绘制收盘价
ax1.plot(etf_daily['trade_date'], etf_daily['close'], label='收盘价', color='#1E40AF')
# 绘制均线
if show_ma:
ax1.plot(etf_daily['trade_date'], etf_daily[f'MA{ma_days}'], label=f'{ma_days}日均线', color='#F97316')
ax1.set_title(f"{selected_etf} 行情走势 ({start_date[:4]}-{start_date[4:6]}-{start_date[6:]} 至 {end_date[:4]}-{end_date[4:6]}-{end_date[6:]})", fontsize=16)
ax1.set_xlabel('日期', fontsize=12)
ax1.set_ylabel('价格 (元)', fontsize=12)
ax1.grid(True, linestyle='--', alpha=0.7)
ax1.legend(loc='upper left')
# 绘制成交量
if show_volume:
ax2 = ax1.twinx()
ax2.bar(etf_daily['trade_date'], etf_daily['vol'], label='成交量', color='#34D399', alpha=0.3)
ax2.set_ylabel('成交量 (手)', fontsize=12)
ax2.legend(loc='upper right')
plt.tight_layout()
st.pyplot(fig)
# 显示最近数据
st.subheader("最近行情数据")
recent_data = etf_daily.sort_values('trade_date', ascending=False).head(10)
recent_data = recent_data[['trade_date', 'open', 'high', 'low', 'close', 'vol']]
recent_data = recent_data.rename(columns={
'trade_date': '日期',
'open': '开盘价',
'high': '最高价',
'low': '最低价',
'close': '收盘价',
'vol': '成交量'
})
st.dataframe(recent_data.style.format({
'开盘价': '{:.3f}',
'最高价': '{:.3f}',
'最低价': '{:.3f}',
'收盘价': '{:.3f}'
}).background_gradient(
subset=['开盘价', '最高价', '最低价', '收盘价'],
cmap='coolwarm'
))
elif menu_option == "金融指标计算":
col1, col2 = st.columns([1, 3])
with col1:
st.subheader("ETF 选择")
selected_etf = st.selectbox(
"请选择ETF计算指标",
basic['name'].tolist(),
index=0
)
etf_code = basic[basic['name'] == selected_etf]['ts_code'].values[0]
st.markdown("---")
st.subheader("计算周期")
calc_period = st.selectbox(
"选择计算周期",
["近7天", "近30天", "近90天", "近180天", "全部"],
index=1
)
# 根据选择确定日期范围
if calc_period == "近7天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=7)).strftime('%Y%m%d')
elif calc_period == "近30天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=30)).strftime('%Y%m%d')
elif calc_period == "近90天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=90)).strftime('%Y%m%d')
elif calc_period == "近180天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=180)).strftime('%Y%m%d')
else:
start_date = daily['trade_date'].min()
end_date = daily['trade_date'].max()
st.markdown("---")
st.subheader("指标设置")
show_return = st.checkbox("显示收益率", value=True)
show_volatility = st.checkbox("显示波动率", value=True)
show_max_drawdown = st.checkbox("显示最大回撤", value=True)
st.markdown("---")
st.button("计算指标")
with col2:
# 筛选数据
etf_daily = daily[(daily['ts_code'] == etf_code) &
(daily['trade_date'] >= start_date) &
(daily['trade_date'] <= end_date)].copy()
# 合并复权因子
etf_daily = pd.merge(etf_daily, adj[adj['ts_code'] == etf_code], on='trade_date')
etf_daily['adj_close'] = etf_daily['close'] * etf_daily['adj_factor']
# 计算指标
st.subheader(f"{selected_etf} 金融指标 ({calc_period})")
# 1. 收益率
if show_return:
first_price = etf_daily['adj_close'].iloc[0]
last_price = etf_daily['adj_close'].iloc[-1]
total_return = (last_price / first_price - 1) * 100
# 计算日收益率
etf_daily['daily_return'] = etf_daily['adj_close'].pct_change()
annual_return = etf_daily['daily_return'].mean() * 252 * 100 # 年化收益率
col1, col2 = st.columns(2)
with col1:
st.metric("总收益率", f"{total_return:.2f}%")
with col2:
st.metric("年化收益率", f"{annual_return:.2f}%")
# 绘制收益率曲线
fig, ax = plt.subplots(figsize=(12, 4))
etf_daily['cum_return'] = (1 + etf_daily['daily_return']).cumprod() - 1
ax.plot(etf_daily['trade_date'], etf_daily['cum_return'] * 100, color='#1E40AF')
ax.set_title("累积收益率曲线", fontsize=14)
ax.set_xlabel("日期", fontsize=12)
ax.set_ylabel("收益率 (%)", fontsize=12)
ax.grid(True, linestyle='--', alpha=0.7)
st.pyplot(fig)
# 2. 波动率
if show_volatility:
volatility = etf_daily['daily_return'].std() * np.sqrt(252) * 100 # 年化波动率
st.subheader("波动率分析")
col1, col2 = st.columns(2)
with col1:
st.metric("年化波动率", f"{volatility:.2f}%")
with col2:
st.metric("平均日波动率", f"{etf_daily['daily_return'].std() * 100:.2f}%")
# 绘制波动率分布
fig, ax = plt.subplots(figsize=(12, 4))
sns.histplot(etf_daily['daily_return'] * 100, kde=True, ax=ax, color='#F97316')
ax.set_title("日收益率分布", fontsize=14)
ax.set_xlabel("日收益率 (%)", fontsize=12)
ax.set_ylabel("频率", fontsize=12)
st.pyplot(fig)
# 3. 最大回撤
if show_max_drawdown:
etf_daily['cummax'] = etf_daily['adj_close'].cummax()
etf_daily['drawdown'] = (etf_daily['adj_close'] / etf_daily['cummax'] - 1) * 100
max_drawdown = etf_daily['drawdown'].min()
st.subheader("最大回撤分析")
st.metric("最大回撤", f"{max_drawdown:.2f}%")
# 绘制回撤曲线
fig, ax = plt.subplots(figsize=(12, 4))
ax.fill_between(etf_daily['trade_date'], etf_daily['drawdown'], 0, color='red', alpha=0.3)
ax.plot(etf_daily['trade_date'], etf_daily['drawdown'], color='red', linewidth=1)
ax.set_title("回撤曲线", fontsize=14)
ax.set_xlabel("日期", fontsize=12)
ax.set_ylabel("回撤 (%)", fontsize=12)
ax.grid(True, linestyle='--', alpha=0.7)
st.pyplot(fig)
elif menu_option == "多ETF对比":
st.subheader("多ETF对比分析")
col1, col2 = st.columns([1, 3])
with col1:
st.subheader("ETF 选择")
selected_etfs = st.multiselect(
"请选择要对比的ETF(最多3个)",
basic['name'].tolist(),
default=['沪深300ETF', '中证500ETF', '创业板ETF']
)
# 限制最多选择3个
if len(selected_etfs) > 3:
st.warning("最多只能选择3个ETF进行对比")
selected_etfs = selected_etfs[:3]
st.markdown("---")
st.subheader("对比周期")
compare_period = st.selectbox(
"选择对比周期",
["近30天", "近90天", "近180天", "近1年"],
index=1
)
# 根据选择确定日期范围
if compare_period == "近30天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=30)).strftime('%Y%m%d')
elif compare_period == "近90天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=90)).strftime('%Y%m%d')
elif compare_period == "近180天":
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=180)).strftime('%Y%m%d')
else:
start_date = (datetime.strptime(daily['trade_date'].max(), '%Y%m%d') - timedelta(days=365)).strftime('%Y%m%d')
end_date = daily['trade_date'].max()
st.markdown("---")
st.subheader("对比指标")
compare_metrics = st.multiselect(
"选择要对比的指标",
["收益率", "波动率", "最大回撤", "成交量"],
default=["收益率", "波动率"]
)
st.markdown("---")
st.button("开始对比")
with col2:
if not selected_etfs:
st.warning("请至少选择一个ETF进行对比")
else:
# 1. 价格走势对比
st.subheader("价格走势对比")
fig, ax = plt.subplots(figsize=(12, 6))
for etf_name in selected_etfs:
etf_code = basic[basic['name'] == etf_name]['ts_code'].values[0]
# 筛选数据
etf_data = daily[(daily['ts_code'] == etf_code) &
(daily['trade_date'] >= start_date) &
(daily['trade_date'] <= end_date)].copy()
# 转换日期格式
etf_data['trade_date'] = pd.to_datetime(etf_data['trade_date'], format='%Y%m%d')
# 归一化价格(以第一天为基准)
first_price = etf_data['close'].iloc[0]
etf_data['normalized_price'] = etf_data['close'] / first_price
# 绘制
ax.plot(etf_data['trade_date'], etf_data['normalized_price'], label=etf_name, linewidth=2)
ax.set_title(f"ETF价格走势对比 ({compare_period})", fontsize=16)
ax.set_xlabel("日期", fontsize=12)
ax.set_ylabel("归一化价格", fontsize=12)
ax.grid(True, linestyle='--', alpha=0.7)
ax.legend()
st.pyplot(fig)
# 2. 指标对比表格
st.subheader("关键指标对比")
metrics_data = []
for etf_name in selected_etfs:
etf_code = basic[basic['name'] == etf_name]['ts_code'].values[0]
# 筛选数据
etf_data = daily[(daily['ts_code'] == etf_code) &
(daily['trade_date'] >= start_date) &
(daily['trade_date'] <= end_date)].copy()
# 合并复权因子
etf_data = pd.merge(etf_data, adj[adj['ts_code'] == etf_code], on='trade_date')
etf_data['adj_close'] = etf_data['close'] * etf_data['adj_factor']
# 计算指标
first_price = etf_data['adj_close'].iloc[0]
last_price = etf_data['adj_close'].iloc[-1]
total_return = (last_price / first_price - 1) * 100
etf_data['daily_return'] = etf_data['adj_close'].pct_change()
volatility = etf_data['daily_return'].std() * np.sqrt(252) * 100
etf_data['cummax'] = etf_data['adj_close'].cummax()
etf_data['drawdown'] = (etf_data['adj_close'] / etf_data['cummax'] - 1) * 100
max_drawdown = etf_data['drawdown'].min()
avg_volume = etf_data['vol'].mean()
metrics_data.append({
'ETF名称': etf_name,
'总收益率 (%)': total_return,
'年化波动率 (%)': volatility,
'最大回撤 (%)': max_drawdown,
'平均成交量 (手)': avg_volume
})
metrics_df = pd.DataFrame(metrics_data)
# 只显示用户选择的指标
display_columns = ['ETF名称'] + [col for col in metrics_df.columns if col != 'ETF名称' and col.split(' ')[0] in compare_metrics]
metrics_df = metrics_df[display_columns]
# 格式化显示
format_dict = {}
for col in metrics_df.columns:
if '%' in col:
format_dict[col] = '{:.2f}'
elif '成交量' in col:
format_dict[col] = '{:,.0f}'
st.dataframe(metrics_df.style.format(format_dict).background_gradient(
subset=[col for col in metrics_df.columns if col != 'ETF名称'],
cmap='coolwarm'
))
# 3. 相关性分析(如果选择了多个ETF)
if len(selected_etfs) > 1:
st.subheader("相关性分析")
corr_data = pd.DataFrame()
for etf_name in selected_etfs:
etf_code = basic[basic['name'] == etf_name]['ts_code'].values[0]
# 筛选数据并计算收益率
etf_data = daily[(daily['ts_code'] == etf_code) &
(daily['trade_date'] >= start_date) &
(daily['trade_date'] <= end_date)].copy()
etf_data['daily_return'] = etf_data['close'].pct_change()
corr_data[etf_name] = etf_data['daily_return']
# 计算相关系数矩阵
corr_matrix = corr_data.corr()
# 绘制热图
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f', ax=ax)
ax.set_title("ETF收益率相关性矩阵", fontsize=14)
st.pyplot(fig)
# 页脚设计
st.markdown("---")
st.markdown(
"""
<div style="text-align: center; color: #6B7280; font-size: 14px;">
ETF数据可视化分析系统 © 2025 团队成员C设计 | 基于Streamlit构建
</div>
""",
unsafe_allow_html=True
)
运行说明
安装依赖:
bash
pip install streamlit pandas numpy matplotlib seaborn
保存代码:
将上述代码复制到文本编辑器,保存为app.py
启动应用:
bash
streamlit run app.py
功能说明:
- 侧边栏导航:切换不同功能模块
- 基本信息查询:查看 ETF 基础数据和跟踪指数
- 行情走势分析:可视化价格走势和成交量
- 金融指标计算:计算收益率、波动率、最大回撤等
- 多 ETF 对比:多维度比较不同 ETF 表现
(UI 设计)重点关注部分
- 整体风格:深蓝色主题,专业金融风格
- 布局结构:
- 侧边栏导航(成员 C 设计)
- 多选项卡内容展示(成员 C 设计)
- 左右分栏布局(左侧筛选,右侧展示)
- 美化细节:
- 自定义标题样式
- 按钮颜色 / 大小优化
- 表格样式美化
- 图表标题 / 标签优化
如果需要调整 UI 风格,只需修改代码中的 CSS 部分(搜索 <style>
标签),改颜色值或字体即可!
学习 Streamlit 看板开发可以按照以下系统化路径进行,结合理论学习、实践项目和资源整合,快速掌握核心技能:
一、入门阶段(0-2 周)
1. 环境搭建与基础语法
安装 Streamlit:
bash
pip install streamlit
运行第一个应用:
创建app.py
文件,写入:python
运行
import streamlit as st st.title("Hello Streamlit!") st.write("这是我的第一个 Streamlit 应用")
终端运行:
streamlit run app.py
,浏览器会自动打开http://localhost:8501
查看效果。掌握核心组件:
学习基础组件的用法(以下代码可直接运行体验):python
运行
import streamlit as st import pandas as pd # 文本组件 st.header("1. 文本展示") st.write("普通文本") st.markdown("**粗体文本**") st.code("print('Hello World')") # 数据展示 st.header("2. 数据可视化") df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) st.dataframe(df) st.line_chart(df) # 交互组件 st.header("3. 用户交互") name = st.text_input("输入你的名字") age = st.slider("选择年龄", 0, 100, 25) hobby = st.selectbox("爱好", ["阅读", "编程", "运动"]) if st.button("提交"): st.success(f"你好,{name}!你{age}岁,喜欢{hobby}。")
2. 官方文档快速上手
- 阅读 Streamlit 官方教程,重点掌握:
- 布局组件(
st.columns
,st.expander
,st.sidebar
) - 图表绘制(
st.plotly_chart
,st.altair_chart
) - 缓存机制(
@st.cache_data
) - 状态管理(
st.session_state
)
- 布局组件(
二、实战阶段(3-4 周)
1. 模仿项目练手
项目 1:简单数据看板
用公开数据集(如 Kaggle 的销售数据、天气数据)制作看板,包含:- 筛选器(时间范围、类别)
- 核心指标卡(总销售额、平均价格)
- 趋势图表(折线图、柱状图)
- 数据表格与下载功能
示例代码框架:
python
运行
import streamlit as st import pandas as pd import plotly.express as px # 数据加载与缓存 @st.cache_data def load_data(): return pd.read_csv("sales_data.csv") data = load_data() # 侧边栏筛选 st.sidebar.header("筛选条件") category = st.sidebar.multiselect( "选择类别", options=data["category"].unique(), default=data["category"].unique() ) # 数据筛选 filtered_data = data[data["category"].isin(category)] # 主页面 st.title("📊 销售数据分析看板") # 指标卡 col1, col2, col3 = st.columns(3) col1.metric("总销售额", f"¥{filtered_data['sales'].sum():,.0f}") col2.metric("销售量", f"{filtered_data['quantity'].sum():,.0f}件") col3.metric("客单价", f"¥{filtered_data['sales'].mean():,.2f}") # 图表 st.subheader("销售趋势") fig = px.line( filtered_data.groupby("date")["sales"].sum().reset_index(), x="date", y="sales", title="每日销售额" ) st.plotly_chart(fig) # 数据表格 st.subheader("详细数据") st.dataframe(filtered_data)
项目 2:机器学习预测界面
用 sklearn 训练简单模型(如房价预测、分类器),再用 Streamlit 构建交互界面,让用户输入特征值并获取预测结果。
2. 参考优秀案例
- 浏览 Streamlit Gallery,学习他人的布局设计和交互逻辑。
- 分析热门项目的代码结构(点击案例下方的
View code
按钮),例如:
三、进阶阶段(5-8 周)
1. 高级功能学习
状态管理:
使用st.session_state
在页面间传递数据或保存用户操作,例如:python
运行
if "counter" not in st.session_state: st.session_state.counter = 0 if st.button("点击计数"): st.session_state.counter += 1 st.write(f"点击次数: {st.session_state.counter}")
自定义组件:
学习使用 Streamlit Elements 或开发自定义组件,例如添加富文本编辑器、文件管理器等。部署优化:
将应用部署到 Streamlit Cloud 或自建服务器,并配置:- 环境变量管理(如 API 密钥)
- 定时数据更新(使用 cron 或云函数)
- 用户认证(如 OAuth、基本身份验证)
2. 实战项目开发
选择一个完整项目深入实践,例如:
- 个人投资追踪工具:连接股票 API,展示资产组合、收益曲线。
- 实验数据可视化平台:科研人员上传数据,自动生成统计图表和报告。
- 企业内部管理看板:整合 HR、财务、项目进度数据(需模拟数据)。
四、资源整合与避坑指南
1. 学习资源推荐
- 官方资源:
- Streamlit 文档(必看)
- Streamlit 社区论坛(遇到问题先搜索)
- 书籍与教程:
- 《Building Data Apps with Streamlit and Python》
- YouTube 频道:Data Professor
- GitHub 项目:
- awesome-streamlit:高质量组件和项目集合
2. 常见问题解决方案
性能优化:
- 大文件读取使用
@st.cache_data
- 避免在循环中重复请求 API
- 使用
st.lazy
延迟加载非关键组件
- 大文件读取使用
布局适配:
- 移动端优先设计:使用
st.columns
时设置gap="small"
- 用
st.experimental_memo
缓存复杂计算
- 移动端优先设计:使用
调试技巧:
- 使用
st.write()
或st.json()
打印中间变量 - 异常捕获:
python
运行
try: # 可能出错的代码 except Exception as e: st.error(f"发生错误: {str(e)}")
- 使用
五、持续学习与社区参与
- 关注官方动态:订阅 Streamlit 博客,了解新功能和最佳实践。
- 参加 hackathon:例如 Streamlit 官方举办的 App of the Week 活动,获取反馈和灵感。
- 分享你的作品:将项目发布到 Streamlit Gallery,加入社区讨论,提升影响力。
通过以上路径,你可以在 2 个月内从零基础成长为 Streamlit 看板开发高手。遇到具体问题时(如数据处理、图表定制),随时告诉我,我会提供针对性解决方案!