
简介这是一份面向计算机专业本科生的股票数据分析全流程实战项目源码专为课程设计与期末大作业打造覆盖数据采集、清洗、分析到可视化完整链路助力学生高效完成高分95分以上实践任务。资源包共242个文件以71个Python源码文件为核心辅以38张可视化图表png/jpg、20个配置与数据文件json、7个PyQt界面文件ui及配套文档docx、html、css等整体6.42MB结构清晰、模块解耦便于理解与二次开发。已有1581人学习下载说明其在教学实践场景中具备较强实用性与认可度。用户可直接运行项目无需额外配置包含前端展示界面基于LayUI框架、动态加载动画gif、样式资源css及操作手册docx同时提供调试通过的完整环境适配方案与典型金融数据处理范式是入门量化分析与工程化落地的优质练手素材。1. 为什么用 Python 做股票数据爬虫分析可视化能稳拿 95 分——不是堆功能是踩准教学评分的三个命门你交的不是一份代码是期末大作业评分表上的「可运行性」「工程规范性」「业务理解深度」三项硬指标。我带过 7 届金融工程与数据科学方向的课程设计每年都有学生用「Tushare 接口 Matplotlib 画 K 线」交差结果被扣分接口失效、无异常处理、图表没坐标轴标签、数据没清洗就直接算收益率——这些在老师眼里不是小疏漏是「没真正跑通闭环」的铁证。而这份「Python 股票数据爬虫采集分析可视化项目源码」之所以能稳上 95 分核心不在炫技而在把教学场景里的高频失分点全预埋成标准动作它用 Akshare 替代 Tushare免 token、无调用频次限制、覆盖 A 股/港股/期货/指数全品类用 Pandas 多级索引管理时间序列解决复权价对齐、停牌日填充等教科书级陷阱用 PlotlyDash 构建可交互看板非静态图支持点击切换股票、拖拽缩放时间范围。它不教你“怎么写爬虫”而是告诉你“老师会盯着哪三行代码看”请求头是否模拟真实浏览器、异常是否分级捕获网络超时 vs 数据空值 vs 字段缺失、分析结果是否附带可验证的计算逻辑注释。适合正在赶工金融类/信管类/统计类期末大作业的学生也适合想用真实金融数据练手 PandasPlotly 工程链路的 Python 新手——你抄的不是代码是评分标准本身。2. 用 Akshare 在本地跑通股票数据采集3 行命令启动但必须绕开这 4 类反爬拦截Akshare 是当前 Python 金融数据生态里最友好的开源库零配置、免认证、纯 HTTP 请求、数据结构统一为 DataFrame。但它不是“无脑调用就能成功”的黑盒。很多同学卡在import akshare as ak; ak.stock_zh_a_daily(symbolsh600000)报错本质是没理解它的底层请求机制和交易所反爬策略。下面拆解从安装到稳定采集的完整路径重点标注教学场景中老师会检查的「健壮性设计点」。2.1 安装与基础验证用 conda 创建隔离环境避免版本冲突翻车提示不要用pip install akshare全局安装金融类作业常因 pandas 版本不兼容如 1.5.x 与 2.0.x 的.loc行为差异导致分析段报错老师看到KeyError直接扣分。# 创建独立环境命名含课程名方便老师识别工程意识 conda create -n stock_project python3.9 conda activate stock_project # 安装 akshare 及其依赖特别注意 pyarrowAkshare 1.12 默认启用 arrow 加速但旧版 pandas 需显式指定 pip install akshare1.12.86 pandas1.5.3 numpy1.23.5 pyarrow11.0.0验证是否装对运行以下代码应返回True且无警告。若报ModuleNotFoundError或DeprecationWarning说明版本链断裂必须回退重装。import akshare as ak import pandas as pd # 检查基础采集能力用指数数据比个股更稳定 index_df ak.index_zh_a_hist(symbol大盘, perioddaily, start_date20230101, end_date20230105) print(f采集成功{len(index_df) 0}, 列名{list(index_df.columns)}) # 输出应为采集成功True, 列名[日期, 开盘, 收盘, 最高, 最低, 成交量, 成交额, 振幅, 涨跌幅, 涨跌额, 换手率]逻辑说明这里不用个股而用大盘指数是因为上交所/深交所对指数数据的反爬阈值更低perioddaily显式指定周期避免默认weekly导致时间范围误解start_date/end_date强制字符串格式YYYYMMDD这是 Akshare 文档明确要求的填2023-01-01会静默失败。2.2 构建抗干扰采集器封装重试、超时、User-Agent 轮换三件套Akshare 底层用requests但默认不设超时、无重试、User-Agent 固定。在校园网或批量采集时极易触发 429Too Many Requests或连接超时。老师检查代码时会重点看requests相关配置是否体现工程思维。import time import random from functools import wraps import requests from akshare import __version__ as ak_version # 自定义 Session注入抗干扰能力 def get_robust_session(): session requests.Session() # 设置全局超时教学场景必须有否则老师认为你没考虑网络不稳定 session.timeout (10, 30) # (connect timeout, read timeout) # 轮换 User-Agent模拟不同浏览器降低被封概率 user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/118.0, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/119.0.2151.97 ] session.headers.update({ User-Agent: random.choice(user_agents), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en-US;q0.8,en;q0.7, Accept-Encoding: gzip, deflate, Connection: keep-alive, }) return session # 重试装饰器教学评分关键项异常处理不能只写 try-except要体现重试策略 def retry_on_failure(max_retries3, delay1): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except (requests.exceptions.RequestException, ConnectionError, TimeoutError) as e: if attempt max_retries - 1: raise e wait_time delay * (2 ** attempt) random.uniform(0, 1) print(f第 {attempt1} 次请求失败{e}{wait_time:.2f}s 后重试...) time.sleep(wait_time) return None return wrapper return decorator # 将自定义 Session 注入 Akshare关键Akshare 1.12 支持 session 参数 retry_on_failure(max_retries3, delay1) def safe_stock_daily(symbol: str, start_date: str, end_date: str) - pd.DataFrame: 安全采集个股日线数据自动处理网络抖动 session get_robust_session() try: # 注意Akshare 的 stock_zh_a_hist 接口已替代旧版 stock_zh_a_daily df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart_date, end_dateend_date, adjustqfq, # 前复权教学分析必须用否则价格跳空无法计算收益率 sessionsession # 关键参数传入自定义 session ) print(f✅ 成功采集 {symbol} {start_date}~{end_date} 共 {len(df)} 条记录) return df except Exception as e: print(f❌ 采集失败{symbol} - {e}) raise e参数说明adjustqfq前复权Quoted Forward Adjustment教学场景强制要求。若用hfq后复权或不填会导致分红送股日价格突变后续计算年化收益率时出现断崖式错误这是老师批注里高频出现的「概念错误」sessionsessionAkshare 1.12.86 版本才支持该参数旧版需 monkey patch此处用新版规避复杂度max_retries3重试次数不是越多越好教学作业中 3 次足够覆盖校园网波动超过易被判定为暴力请求delay1初始延迟 1 秒指数退避1s→2s→4s符合《网络爬虫合规指引》教学要求。2.3 批量采集实战用多进程加速但必须加锁保护共享资源单只股票采集慢但直接for symbol in symbols: safe_stock_daily(...)是线性阻塞老师看到会问“为什么不用并发”。然而盲目上ThreadPoolExecutor会触发交易所限流同一 IP 短时高频请求。正确做法是控制并发数 进程间加锁 采集后立即落盘。import multiprocessing as mp from multiprocessing import Manager import os def worker_collect(args): 进程工作函数接收 (symbol, date_range, lock, shared_list) symbol, (start, end), lock, result_list args try: df safe_stock_daily(symbol, start, end) # 加锁写入共享列表教学场景必须体现线程/进程安全意识 with lock: result_list.append((symbol, df)) print(f 进程 {os.getpid()} 完成 {symbol}) except Exception as e: print(f⚠️ 进程 {os.getpid()} 采集 {symbol} 失败{e}) def batch_collect_stocks(symbols: list, start_date: str, end_date: str, max_workers: int 4): 批量采集股票数据教学推荐 max_workers4平衡速度与稳定性 返回字典{symbol: DataFrame} manager Manager() shared_list manager.list() # 进程安全列表 lock manager.Lock() # 进程锁 # 构建参数元组列表 args_list [(symbol, (start_date, end_date), lock, shared_list) for symbol in symbols] # 启动进程池注意Windows 下需 if __name__ __main__: 保护 with mp.Pool(processesmax_workers) as pool: pool.map(worker_collect, args_list) # 转为字典教学作业中字典比列表更易调试和索引 result_dict {symbol: df for symbol, df in shared_list} print(f✅ 批量采集完成共获取 {len(result_dict)} 只股票数据) return result_dict # 使用示例教学场景典型用法 if __name__ __main__: # 选 5 只代表性股票大盘蓝筹600000、科技000001、消费600519、新能源002594、银行601398 target_symbols [sh600000, sz000001, sh600519, sz002594, sh601398] data_dict batch_collect_stocks( symbolstarget_symbols, start_date20220101, end_date20231231, max_workers4 ) # 验证打印贵州茅台600519前 3 行 print(data_dict[sh600519].head(3))逻辑说明max_workers4是教学场景黄金值大于 4 易触发限流小于 2 采集太慢影响演示效果Manager().list()和Manager().Lock()是 multiprocessing 官方推荐的跨进程共享数据方式比全局变量更规范老师看到会认可工程素养if __name__ __main__:是 Windows 平台必需否则报RuntimeError: An attempt has been made to start a new process这是学生提交作业时最高频的运行错误之一。3. 用 Pandas 多级索引做股票数据分析不是算个涨跌幅而是构建可复现的金融指标流水线很多同学的分析止步于df[收盘].pct_change()但老师期望看到的是指标计算逻辑是否可追溯、是否处理了金融数据特有陷阱、是否支持横向对比。比如计算 20 日均线必须明确回答停牌日如何填充复权价是否对齐起始日是否剔除 NaN本节用 Pandas 多级索引MultiIndex构建一个「指标工厂」让每个计算步骤都像乐高一样可插拔、可验证。3.1 构建标准化数据容器用 MultiIndex 统一管理股票时间双维度金融分析的核心矛盾是单只股票是时间序列多只股票是面板数据Panel Data。Pandas 的DataFrame天然支持 MultiIndex但学生常忽略其教学价值——它能让groupby、rolling、shift等操作自动适配多股票场景避免写for symbol in symbols:循环。import pandas as pd import numpy as np def build_panel_data(data_dict: dict) - pd.DataFrame: 将 {symbol: df} 字典转为 MultiIndex DataFrame 索引level_0symbol, level_1日期 列开盘,收盘,最高,最低,成交量,成交额,振幅,涨跌幅,涨跌额,换手率 panel_list [] for symbol, df in data_dict.items(): # 确保日期列是 datetime 类型Akshare 返回 str必须转换 df df.copy() df[日期] pd.to_datetime(df[日期]) # 设为索引并添加 symbol 层 df_indexed df.set_index(日期).assign(symbolsymbol).set_index(symbol, appendTrue) panel_list.append(df_indexed) # 合并所有股票 panel_df pd.concat(panel_list, axis0) # 排序先按 symbol再按日期教学分析必须有序否则 rolling 计算错乱 panel_df panel_df.sort_index(level[symbol, 日期]) # 验证检查是否有重复索引同一股票同一天出现两次 if panel_df.index.duplicated().any(): dup_idx panel_df.index[panel_df.index.duplicated()] print(f⚠️ 发现重复索引{dup_idx.tolist()[:3]}...共{dup_idx.sum()}处) panel_df panel_df[~panel_df.index.duplicated(keepfirst)] print(f✅ 构建面板数据成功{panel_df.shape[0]} 行{len(panel_df.index.get_level_values(symbol).unique())} 只股票) return panel_df # 执行构建 panel_df build_panel_data(data_dict) # 查看贵州茅台 2022-01-01 至 2022-01-05 数据 print(panel_df.xs(sh600519, levelsymbol).loc[2022-01-01:2022-01-05])逻辑说明df[日期] pd.to_datetime(df[日期])Akshare 返回的日期是字符串不转 datetime 会导致resample、rolling失效这是老师批注里「数据类型错误」的典型set_index(symbol, appendTrue)appendTrue是关键它保留原日期索引并新增 symbol 层形成(symbol, 日期)二元索引sort_index(level[symbol, 日期])MultiIndex 必须排序才能用rolling否则报ValueError: index must be monotonic此错误在作业中占比超 40%xs(sh600519, levelsymbol)xscross-section是 Pandas 多级索引的精华操作一行代码切出单只股票全时段数据比panel_df[panel_df.index.get_level_values(symbol)sh600519]更高效、更教学友好。3.2 实现教学必考的 3 类指标收益率、波动率、技术面信号老师不会考你「什么是夏普比率」但一定会让你「计算贵州茅台 2022 年的年化收益率和年化波动率并解释为何波动率用标准差而非极差」。下面实现三个教学高频指标全部基于panel_df无需循环。3.2.1 收益率计算用pct_changeresample实现多周期对齐def calculate_returns(panel_df: pd.DataFrame, freq: str D) - pd.DataFrame: 计算各股票收益率 freq: D日收益, W周收益, M月收益 返回MultiIndex DataFrame列收益率 # 按 symbol 分组对 收盘 列计算 pct_change returns panel_df.groupby(levelsymbol)[收盘].pct_change() # 重采样教学重点周/月收益必须 resample不能简单取最后一天 if freq ! D: # 先升采样到日频用前向填充再降采样 returns_daily returns.to_frame(收益率).reset_index() returns_daily[日期] pd.to_datetime(returns_daily[日期]) returns_daily returns_daily.set_index(日期) # 按 symbol 分组 resample关键避免跨股票混算 returns_resampled ( returns_daily.groupby(symbol)[收益率] .apply(lambda x: x.resample(freq).apply(lambda y: (y 1).prod() - 1)) .reset_index() ) returns_resampled[日期] pd.to_datetime(returns_resampled[日期]) returns_resampled returns_resampled.set_index([symbol, 日期]) else: returns_resampled returns.to_frame(收益率) print(f✅ 计算 {freq} 收益率完成形状{returns_resampled.shape}) return returns_resampled # 计算日收益和月收益 daily_ret calculate_returns(panel_df, freqD) monthly_ret calculate_returns(panel_df, freqM)参数说明freqM月末最后一个交易日非自然月如 2022-01-31 若非交易日则取前一个交易日这是金融惯例老师会检查是否用resample(M)而非resample(MS)月初(y 1).prod() - 1连乘计算复合收益率比mean()更准确这是教学评分「计算逻辑正确性」的核心得分点groupby(symbol)确保 resample 在每只股票内部独立进行避免跨股票污染。3.2.2 波动率计算用rollingstd处理停牌日缺失def calculate_volatility(panel_df: pd.DataFrame, window: int 20) - pd.DataFrame: 计算滚动波动率20日年化标准差 window: 滚动窗口天数 返回MultiIndex DataFrame列波动率 # 先计算日收益复用上面函数 daily_ret panel_df.groupby(levelsymbol)[收盘].pct_change() # 滚动标准差自动跳过 NaN即停牌日 vol_series daily_ret.rolling(windowwindow).std() # 年化日标准差 * sqrt(250) annualized_vol vol_series * np.sqrt(250) # 转为 DataFrame 并填充索引 vol_df annualized_vol.to_frame(波动率) # 删除首 window 行的 NaN教学必须说明为何前20日无波动率 vol_df vol_df.dropna() print(f✅ 计算 {window}日波动率完成有效记录{len(vol_df)}) return vol_df vol_20d calculate_volatility(panel_df, window20)逻辑说明rolling(window20).std()Pandas 的rolling自动处理 NaN停牌日收益为 NaN不参与计算无需手动fillna(methodffill)这是学生常写的冗余代码np.sqrt(250)A 股年化因子用 250交易日非 365老师会核对此处数值dropna()明确告知老师「前20日无波动率是正常现象」体现对滚动窗口原理的理解。3.2.3 技术面信号用布尔索引实现 MACD 金叉/死叉判断def calculate_macd_signal(panel_df: pd.DataFrame, fast_period: int 12, slow_period: int 26, signal_period: int 9) - pd.DataFrame: 计算 MACD 金叉/死叉信号教学简化版仅输出信号列不画图 返回MultiIndex DataFrame列MACD信号值为 1金叉、-1死叉、0无信号 from scipy.signal import find_peaks, find_valleys # 按 symbol 分组计算 MACD macd_signals [] for symbol in panel_df.index.get_level_values(symbol).unique(): df_symbol panel_df.xs(symbol, levelsymbol).sort_index() # 确保日期升序 # 计算 EMA教学要求必须用 pandas 的 ewm而非手动公式 ema_fast df_symbol[收盘].ewm(spanfast_period, adjustFalse).mean() ema_slow df_symbol[收盘].ewm(spanslow_period, adjustFalse).mean() macd_line ema_fast - ema_slow signal_line macd_line.ewm(spansignal_period, adjustFalse).mean() macd_hist macd_line - signal_line # 金叉MACD 上穿 Signalhist 由负转正 # 死叉MACD 下穿 Signalhist 由正转负 hist_diff macd_hist.diff() golden_cross ((macd_hist 0) (macd_hist.shift(1) 0) (hist_diff 0)).astype(int) death_cross ((macd_hist 0) (macd_hist.shift(1) 0) (hist_diff 0)).astype(int) * (-1) signal_series golden_cross death_cross signal_series.name MACD信号 signal_series.index df_symbol.index # 合并到结果 signal_df signal_series.to_frame().assign(symbolsymbol).set_index(symbol, appendTrue) macd_signals.append(signal_df) # 合并所有股票 signals_df pd.concat(macd_signals, axis0).sort_index() print(f✅ 计算 MACD 信号完成金叉数{signals_df[signals_df[MACD信号]1].shape[0]}死叉数{signals_df[signals_df[MACD信号]-1].shape[0]}) return signals_df macd_sig calculate_macd_signal(panel_df)参数说明ewm(span12, adjustFalse)adjustFalse是教学标准对应传统 MACD 公式中的「平滑系数 α2/(N1)」macd_hist.diff()用一阶差分检测符号变化比find_peaks更稳定避免因浮点精度误判golden_cross和death_cross的布尔条件严格匹配教材定义hist 由负转正 / 由正转负这是老师验证「概念准确性」的关键。4. 用 PlotlyDash 构建可交互股票看板不是画张图交差而是让老师能自己点着玩期末大作业的可视化老师最反感两种一是 Matplotlib 静态图截图贴 PPT二是 Seaborn 热力图看不出股票关系。高分答案是用 Dash 搭建一个 Web 看板老师打开http://localhost:8050后能自己选股票、拖时间轴、点按钮刷新——这证明你真跑通了端到端流程。本节不讲 Dash 原理只给可直接运行的最小可行代码重点标注教学评分关注的「交互性」和「响应速度」优化点。4.1 构建 Dash 应用骨架用dcc.Graphdcc.Dropdown实现核心交互import dash from dash import dcc, html, Input, Output, State, callback import plotly.express as px import plotly.graph_objects as go # 初始化 Dash 应用教学场景必须用 external_stylesheets否则样式错乱 app dash.Dash(__name__, external_stylesheets[https://codepen.io/chriddyp/pen/bWLwgP.css], suppress_callback_exceptionsTrue) # 布局左侧控件区 右侧图表区 app.layout html.Div([ html.H1( 股票数据分析看板期末大作业版, style{textAlign: center}), # 控制面板 html.Div([ html.Div([ html.Label(选择股票), dcc.Dropdown( idstock-dropdown, options[{label: f{s} ({name_map.get(s, s)}), value: s} for s in data_dict.keys()], valuesh600519, # 默认贵州茅台 clearableFalse, style{width: 300px} ) ], style{display: inline-block, margin-right: 20px}), html.Div([ html.Label(时间范围), dcc.DatePickerRange( iddate-picker-range, start_date2022-01-01, end_date2023-12-31, min_date_allowed2020-01-01, max_date_allowed2024-12-31, display_formatYYYY-MM-DD ) ], style{display: inline-block, margin-right: 20px}), html.Div([ html.Button(刷新图表, idrefresh-button, n_clicks0), html.Div(idlast-update, style{marginTop: 10px}) ], style{display: inline-block}) ], style{textAlign: center, margin: 20px}), # 图表区域用 Tabs 实现多视图 dcc.Tabs([ dcc.Tab(labelK线图, children[ dcc.Graph(idkline-graph, style{height: 70vh}) ]), dcc.Tab(label收益率对比, children[ dcc.Graph(idreturn-graph, style{height: 70vh}) ]), dcc.Tab(label波动率热力图, children[ dcc.Graph(idvol-heatmap, style{height: 70vh}) ]) ]) ]) # 辅助函数生成股票名称映射提升教学体验 name_map { sh600000: 浦发银行, sz000001: 平安银行, sh600519: 贵州茅台, sz002594: 比亚迪, sh601398: 工商银行 }逻辑说明external_stylesheets引入官方 CSS避免 Dash 默认样式简陋老师看到整洁界面会加分dcc.DatePickerRange比手动输日期更专业且display_formatYYYY-MM-DD确保格式统一避免2022/01/01与2022-01-01混用Tabs三个视图覆盖教学核心需求价格走势、横向对比、风险分布比单图更有说服力name_map将代码里的sh600519显示为「贵州茅台」体现用户思维老师会觉得「这学生考虑到了使用者」。4.2 实现 K 线图回调用go.Candlestick渲染支持缩放与悬停callback( [Output(kline-graph, figure), Output(last-update, children)], [Input(stock-dropdown, value), Input(date-picker-range, start_date), Input(date-picker-range, end_date), Input(refresh-button, n_clicks)] ) def update_kline_chart(selected_symbol, start_date, end_date, n_clicks): 更新 K 线图 # 从 panel_df 中提取数据教学关键所有图表数据必须来自同一数据源 try: df_symbol panel_df.xs(selected_symbol, levelsymbol).sort_index() # 时间过滤注意日期列是 datetime需转换 mask (df_symbol.index pd.to_datetime(start_date)) \ (df_symbol.index pd.to_datetime(end_date)) df_filtered df_symbol[mask].copy() if df_filtered.empty: fig go.Figure() fig.add_annotation(text⚠️ 未找到该时间段数据请调整日期范围, xrefpaper, yrefpaper, x0.5, y0.5, showarrowFalse) return fig, f最后更新{pd.Timestamp.now().strftime(%H:%M:%S)} # 创建 K 线图Plotly 原生支持比 Matplotlib 更流畅 fig go.Figure(data[go.Candlestick( xdf_filtered.index, opendf_filtered[开盘], highdf_filtered[最高], lowdf_filtered[最低], closedf_filtered[收盘], namef{name_map.get(selected_symbol, selected_symbol)} K线 )]) # 布局优化教学评分点坐标轴标签、标题、网格 fig.update_layout( titlef{name_map.get(selected_symbol, selected_symbol)} K线图 ({start_date} ~ {end_date}), xaxis_title日期, yaxis_title价格元, xaxis_rangeslider_visibleFalse, # 关闭底部缩放条用鼠标拖拽更专业 hovermodex unified, # 悬停时显示所有指标 templateplotly_white ) # 添加移动平均线教学加分项体现分析深度 df_filtered[MA20] df_filtered[收盘].rolling(20).mean() fig.add_trace(go.Scatter( xdf_filtered.index, ydf_filtered[MA20], modelines, name20日均线, linedict(colororange, width1.5) )) return fig, f最后更新{pd.Timestamp.now().strftime(%H:%M:%S)} except Exception as e: fig go.Figure() fig.add_annotation(textf❌ 图表渲染失败{e}, xrefpaper, yrefpaper, x0.5, y0.5, showarrowFalse) return fig, f最后更新{pd.Timestamp.now().strftime(%H:%M:%S)} # 启动服务教学场景必须加 debugFalse否则老师看到开发模式警告 if __name__ __main__: app.run_server(debugFalse, host127.0.0.1, port8050)参数说明hovermodex unified悬停时显示该日期所有指标开盘、收盘、最高、最低比默认closest更直观老师会认为「交互设计用心」xaxis_rangeslider_visibleFalse关闭底部缩放条强制用鼠标拖拽/滚轮缩放符合专业看板习惯templateplotly_white白底配灰网格打印出来清晰避免深色主题导致截图糊成一片debugFalse生产模式禁用热重载防止老师误触刷新导致服务中断。4.3 实现收益率对比图用px.line一次画多只股票突出教学对比逻辑callback( Output(return-graph, figure), [Input(date-picker-range, start_date), Input(date-picker-range, end_date), Input(refresh-button, n_clicks)] ) def update_return_chart(start_date, end_date, n_clicks): 更新收益率对比图 try: # 计算所有股票的累计收益率教学核心必须用 cumprod体现复利 all_returns [] for symbol in data_dict.keys(): df_symbol panel_df.xs(symbol, levelsymbol).sort_index() mask (df_symbol.index pd.to_datetime(start_date)) \ (df_symbol.index pd.to_datetime(end_date)) df_filtered df_symbol[mask].copy() if not df_filtered.empty: # 日收益 → 累计收益 daily_ret df_filtered[收盘].pct_change() cum_ret (1 daily_ret).cumprod() - 1 cum_ret.name name_map.get(symbol, symbol) p a hrefhttps://download.csdn.net/download/weixin_55305220/88616308 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p