学习日志01 ETF 基础数据可视化分析与简易管理系统

发布于:2025-06-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

我的任务:(UI 设计 + 布局)学习与任务指南!!!!!!!!

一、需要学习的内容(超简单,1-2 小时就能上手)

(一)Streamlit 基础操作(核心工具)

  1. 安装与启动

    • 学什么:怎么让 Streamlit 跑起来,看到自己的界面
    • 怎么做:
      • 打开命令行(Windows 按 Win+R 输入 cmd ;Mac 用 Launchpad 找 “终端” )
      • 输入 pip install streamlit (装 Streamlit 库,只需输 1 次)
      • 输入 streamlit run app.py (启动看板,app.py 是你的代码文件名)
      • 浏览器自动弹出看板页面,就成功啦!
  2. 基础语法(复制即用)

    • 学什么:怎么用几行代码加标题、按钮、下拉框
    • 怎么做:
      • 标题:st.title("我是大标题")
      • 下拉框:selected = st.selectbox("选一个", ["选项1", "选项2"])
      • 按钮:if st.button("点我"): st.write("你点啦!")
      • 直接把这些代码贴到 app.py 里,运行看效果,改文字就能用!

(二)简单界面美化(不用学 CSS !)

  1. 改标题颜色 / 字体

    • 学什么:让标题更醒目,不用懂复杂样式代码
    • 怎么做:复制这段代码到 app.py ,改颜色值(比如 #FF9933 换成你喜欢的色号)

      python

      运行

      st.markdown(
          "<h1 style='color: #FF9933; font-family: 黑体;'>ETF 看板</h1>",
          unsafe_allow_html=True
      )
      
  2. 改按钮样式

    • 学什么:让按钮变好看,适配金融主题
    • 怎么做:复制这段代码,改颜色(#1E90FF 是深蓝色,可换成 #FF5733 橙色)

      python

      运行

      st.markdown(
          "<style>div.stButton > button {background-color: #1E90FF; color: white; font-size: 16px;}</style>",
          unsafe_allow_html=True
      )
      

(三)手绘原型图(用 WPS 就能做)

  1. 学什么:怎么快速画出界面布局,让队友清楚你的设计
  2. 怎么做
    • 打开 WPS 演示(或 PowerPoint )→ 新建空白页
    • 用 “矩形”“文本框” 拼出界面:
      • 画个大矩形当 “顶部标题栏”,写 “ETF 数据看板”
      • 左边画小矩形当 “ETF 选择区”,放下拉框 + 按钮
      • 右边分上下,画折线图(用 “曲线” 模拟)和表格(用 “直线” 拼)
    • 保存为 界面原型草图.jpg ,发给队友!

二、具体任务拆解(按天做,超清晰)

Day3:界面基础美化 + 手绘原型

上午:让看板 “好看起来”(2 小时)
  1. 启动 Streamlit

    • 找到团队共享的 app.py (或自己新建一个,复制基础代码 )
    • 命令行输入 streamlit run app.py ,看到默认界面
  2. 加页面配置

    • 打开 app.py ,开头加这段代码(复制):

      python

      运行

      import streamlit as st
      st.set_page_config(
          page_title="ETF 看板", 
          page_icon="📊", 
          layout="wide"  # 宽屏,内容不挤
      )
      
    • 重启 Streamlit,看浏览器标签和界面变宽!
  3. 美化标题和按钮

    • 在 app.py 里加标题美化代码(改颜色 / 字体)
    • 加按钮美化代码(改按钮颜色)
    • 保存后,刷新页面看变化,调整到满意
下午:手绘原型图(2 小时)
  1. 打开 WPS 演示,新建空白页
  2. 画界面布局
    • 顶部:大标题 “ETF 数据可视化看板”
    • 左侧:下拉框(选 ETF )+ 查询按钮
    • 右侧上:日线行情折线图(用曲线画)
    • 右侧下:分钟行情预览表格(用直线拼)
  3. 保存为 界面原型草图.jpg ,发给队友(成员 D、组长)

Day5:多选项卡功能扩展

上午:给看板加 “选项卡”(2 小时)
  1. 复制选项卡代码

    • 打开 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 填指标代码
      
  2. 调整内容排版

    • 在每个选项卡内的图表 / 下拉框前,加 st.markdown("<br>", unsafe_allow_html=True) (加空行,让内容不挤)
    • 给图表加标题,比如 st.subheader("日线行情(近 30 天)")
  3. 验证效果

    • 重启 Streamlit,切换选项卡,看内容是否正常显示
    • 截图发给队友,同步布局调整后的样子
下午:适配与优化(2 小时)
  1. 根据队友反馈调整

    • 如果成员 D 说 “指标模块放不下”,就在选项卡 3 里加 st.expander("点击看指标") 做折叠
    • 如果界面太丑,再改改标题颜色、按钮大小
  2. 更新原型图

    • 打开 WPS ,在原来的原型图上,标记 “选项卡 1/2/3” 对应的功能
    • 保存为 界面原型草图_更新版.jpg ,发群里

三、遇到问题怎么办?(超全解决方案)

  1. Streamlit 启动报错

    • 问题:命令行显示 找不到 app.py
    • 解决:先 cd 到代码所在文件夹(比如 cd D:\ETF项目 ),再运行 streamlit run app.py
  2. 美化代码不生效

    • 问题:改了颜色,页面没变化
    • 解决:检查代码里有没有 unsafe_allow_html=True ,重启 Streamlit(ctrl+C 终止,再重新运行 )
  3. 手绘原型太丑

    • 解决:用 WPS 演示的 “模板”→ 选 “商务汇报” 类,直接改文字和布局,更简单!

四、你要输出的成果(超有成就感)

  1. Day3 结束

    • 美化后的 Streamlit 看板(有自定义标题、按钮样式,宽屏布局 )
    • 界面原型草图.jpg (手绘 / WPS 做的界面布局图 )
  2. 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
)

运行说明

  1. 安装依赖

    bash

    pip install streamlit pandas numpy matplotlib seaborn
    
  2. 保存代码
    将上述代码复制到文本编辑器,保存为 app.py

  3. 启动应用

    bash

    streamlit run app.py
    
  4. 功能说明

    • 侧边栏导航:切换不同功能模块
    • 基本信息查询:查看 ETF 基础数据和跟踪指数
    • 行情走势分析:可视化价格走势和成交量
    • 金融指标计算:计算收益率、波动率、最大回撤等
    • 多 ETF 对比:多维度比较不同 ETF 表现

(UI 设计)重点关注部分

  1. 整体风格:深蓝色主题,专业金融风格
  2. 布局结构
    • 侧边栏导航(成员 C 设计)
    • 多选项卡内容展示(成员 C 设计)
    • 左右分栏布局(左侧筛选,右侧展示)
  3. 美化细节
    • 自定义标题样式
    • 按钮颜色 / 大小优化
    • 表格样式美化
    • 图表标题 / 标签优化

如果需要调整 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.columnsst.expanderst.sidebar
    • 图表绘制(st.plotly_chartst.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. 参考优秀案例

三、进阶阶段(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. 学习资源推荐
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 看板开发高手。遇到具体问题时(如数据处理、图表定制),随时告诉我,我会提供针对性解决方案!


网站公告

今日签到

点亮在社区的每一天
去签到