简介:这是一套面向高校计算机与金融工程专业学生的Python股票数据分析全流程实践项目,适用于课程大作业、毕业设计或量化入门学习。资源完整实现从网页爬取实时/历史股票数据、清洗分析到多维度可视化展示的端到端功能,代码经实测可直接运行,含详细注释与模块化结构,便于理解与二次开发。压缩包共362个文件,主体为38个核心Python脚本(涵盖requests爬虫、pandas数据处理、matplotlib/seaborn/plotly可视化)、59个HTML交互式报告页、39张PNG图表截图及148个GIF动图演示关键操作流程,辅以JS/CSS前端渲染支持,整体包体仅9.16MB,轻量易部署。目前已有1805人学习下载,项目结构清晰,包含配置文件(ini/json)、SQL建表语句、Shell部署脚本及LayUI等前端框架样式资源,兼顾后端逻辑与前端展示,是少有的集爬虫、分析、可视化于一体的高分(95+)教学级实战源码。
1. 这不是又一个“爬股票然后画图”的玩具项目:它是一套可直接嵌入实盘监控流程的轻量级数据管道
如果你在期末大作业里交了一份用akshare爬几只股票、pandas算个涨跌幅、matplotlib画三条线的脚本,老师给85分——那这份95分以上的源码包,大概率会让你重新理解“框架”二字的分量。它不依赖 Jupyter Notebook 的交互式幻觉,也不靠手动改代码来切换股票池;它把数据获取、清洗校验、指标计算、存储归档、图表渲染全部封装成可配置、可调度、可复用的模块。核心逻辑跑在纯 Python 环境下,无 Web 服务依赖,命令行一键触发全链路,输出结果自动存入 SQLite 并生成带时间戳的 HTML 报告。适合金融工程课设、量化兴趣小组快速验证策略逻辑、甚至作为实习中搭建本地监控看板的起点。对新手,它暴露了真实金融数据流中的脏数据陷阱(如停牌日缺失、复权因子跳变、交易所休市标识混乱);对有经验者,它的IndicatorEngine类设计和DataRouter配置机制,提供了比backtrader更透明、比zipline更轻量的二次开发入口。
2. 用 requests + akshare 构建健壮的股票数据爬虫层:绕过反爬、处理断点、统一字段语义
2.1 为什么不用 tushare 或 baostock?选型依据与协议层控制权
tushare免费版接口调用频次受限且部分字段需付费解锁;baostock依赖独立客户端进程,部署隔离性差;而akshare基于 HTTP 协议直连交易所/财经门户原始页面,源码完全开源,字段命名贴近原始披露口径(如trade_date对应上交所日志字段),且支持retry_times=3、timeout=15等底层参数透传。更重要的是,该框架将akshare封装为StockDataFetcher类,所有请求均通过自定义Session实例发出,并注入User-Agent和Referer头模拟浏览器行为:
# data/fetcher.py import akshare as ak import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry class StockDataFetcher: def __init__(self): self.session = requests.Session() retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount("http://", adapter) self.session.mount("https://", adapter) self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.sse.com.cn/" }) def fetch_daily(self, symbol: str, start_date: str, end_date: str) -> pd.DataFrame: try: df = ak.stock_zh_a_hist(symbol=symbol, period="daily", start_date=start_date, end_date=end_date, adjust="qfq") # 前复权确保价格连续 except Exception as e: logger.error(f"Fetch failed for {symbol}: {e}") return pd.DataFrame() return self._normalize_columns(df) def _normalize_columns(self, df: pd.DataFrame) -> pd.DataFrame: # 统一字段名:akshare 输出列名不一致(如 '日期'/'交易日期'/'date') rename_map = { "日期": "trade_date", "交易日期": "trade_date", "date": "trade_date", "开盘": "open", "最高": "high", "最低": "low", "收盘": "close", "成交量": "volume", "成交额": "amount" } df = df.rename(columns=rename_map) df["trade_date"] = pd.to_datetime(df["trade_date"]).dt.strftime("%Y-%m-%d") return df.sort_values("trade_date").reset_index(drop=True)提示:
ak.stock_zh_a_hist的adjust="qfq"参数必须显式指定,否则默认返回未复权数据,导致技术指标(如 MA、MACD)在除权日出现断崖式跳空。框架在fetch_daily方法末尾强制调用_normalize_columns,解决不同股票代码(如 000001.SZ 与 600000.SH)返回列名不一致问题——这是学生项目中最常被忽略的“数据语义漂移”。
2.2 断点续爬与增量更新:避免每次全量拉取浪费带宽与时间
框架引入SQLite表stock_metadata记录每只股票最新已入库交易日。DataPipeline.run()执行时,先查表获取max(trade_date),再向fetch_daily传入start_date = max_date + pd.Timedelta(days=1)。若某次网络中断导致000001.SZ只拉到 2024-03-15,下次运行不会重拉 2024-01-01 至 2024-03-15,而是从 2024-03-16 开始:
# pipeline.py def _get_latest_date(self, symbol: str) -> Optional[str]: with sqlite3.connect(self.db_path) as conn: cursor = conn.cursor() cursor.execute( "SELECT MAX(trade_date) FROM stock_data WHERE symbol = ?", (symbol,) ) result = cursor.fetchone()[0] return result if result else "2020-01-01" def run(self, symbols: List[str]): for symbol in symbols: latest_date = self._get_latest_date(symbol) today = datetime.now().strftime("%Y-%m-%d") if latest_date >= today: logger.info(f"{symbol} already up to date") continue df = self.fetcher.fetch_daily(symbol, latest_date, today) if not df.empty: self._save_to_db(df, symbol)注意:
_get_latest_date返回"2020-01-01"作为兜底值,而非None,避免pd.date_range在start_date=None时抛出异常。该设计使首次运行自动全量抓取,后续运行严格增量——这是生产级数据管道的底线要求,远超课程作业常见的“每次删库重跑”。
2.3 处理三大典型脏数据:停牌、一字板、交易所休市日
原始数据中,akshare对停牌日通常返回空 DataFrame 或填充NaN,但volume=0与close=open=high=low的一字板需区分对待。框架在DataCleaner中定义三类校验规则:
| 触发条件 | 处理动作 | 业务含义 |
|---|---|---|
volume == 0且close == open == high == low | 保留该行,标记status = "limit_up"或"limit_down" | 一字涨停/跌停,属有效交易状态 |
volume == 0且close != open | 删除该行,记录警告WARN: price_change_without_volume | 数据异常,可能为临时停牌或接口错误 |
trade_date不在akshare.get_trade_days()返回列表中 | 删除该行,记录WARN: non_trading_day_in_data | 交易所休市日误入数据 |
# data/cleaner.py def clean_daily_data(self, df: pd.DataFrame) -> pd.DataFrame: trade_days = ak.get_trade_days(start_date="20200101", end_date="20300101") trade_days = [d.strftime("%Y-%m-%d") for d in trade_days] df = df[df["trade_date"].isin(trade_days)].copy() df["status"] = "normal" # 标记一字板 is_limit = (df["volume"] == 0) & (df["close"] == df["open"]) & (df["close"] == df["high"]) & (df["close"] == df["low"]) df.loc[is_limit & (df["close"] > df["close"].shift(1)), "status"] = "limit_up" df.loc[is_limit & (df["close"] < df["close"].shift(1)), "status"] = "limit_down" # 删除无效零成交量 invalid_zero_vol = (df["volume"] == 0) & (df["close"] != df["open"]) df = df[~invalid_zero_vol].reset_index(drop=True) return df这套规则让清洗后的数据可直接用于talib计算 MACD,避免因停牌日NaN导致整列指标失效——这是95分项目与85分项目的分水岭。
3. 基于 pandas-ta 的指标计算引擎:支持自定义公式、批量回测、结果缓存
3.1 为什么弃用 talib?pandas-ta 的可调试性与国产适配优势
ta-lib编译依赖复杂,在 Windows 上常因Microsoft Visual C++ 14.0缺失报错;其 C 扩展函数无法单步调试,当MACD计算结果与通达信不一致时,学生难以定位是参数fastperiod=12设置错误,还是signalperiod=9与源平台默认值不同。pandas-ta完全基于 NumPy/Pandas 实现,所有指标函数均为 Python 源码,可直接import pandas_ta as ta; print(ta.macd.__code__)查看逻辑。框架将其封装为IndicatorEngine,支持两种调用模式:
- 声明式配置:在
config/indicators.yaml中定义指标族 - 编程式扩展:继承
BaseIndicator类实现自定义指标(如“量能饱和度圆圈”)
# config/indicators.yaml macd: module: "pandas_ta" function: "macd" params: fast: 12 slow: 26 signal: 9 rsi: module: "pandas_ta" function: "rsi" params: length: 14 custom_saturation: module: "indicators.saturation" function: "saturation_circle" params: window: 203.2 批量计算与缓存机制:避免重复解析同一段行情
IndicatorEngine.compute_batch()接收symbol_list和date_range,先检查cache/indicators/{symbol}_{start}_{end}.parquet是否存在。若存在且mtime > last_fetch_time,则直接加载;否则调用pandas_ta计算并保存。Parquet 格式比 CSV 节省 60% 存储空间,且支持按列读取(如只需macd_macd列时,不加载rsi列):
# indicators/engine.py def compute_batch(self, symbols: List[str], start_date: str, end_date: str): cache_key = f"{'_'.join(symbols)}_{start_date}_{end_date}" cache_path = Path("cache/indicators") / f"{cache_key}.parquet" if cache_path.exists(): mtime = datetime.fromtimestamp(cache_path.stat().st_mtime) if mtime > self.last_fetch_time: logger.info(f"Load indicators from cache: {cache_path}") return pd.read_parquet(cache_path) # 合并多只股票数据,统一计算(提升 pandas-ta 向量化效率) all_df = pd.concat([ self._load_symbol_data(sym, start_date, end_date) for sym in symbols ], ignore_index=True) for indicator_name, config in self.config.items(): module = importlib.import_module(config["module"]) func = getattr(module, config["function"]) params = config["params"] # pandas-ta 支持直接传入 DataFrame 并添加新列 all_df = func(all_df, **params) cache_path.parent.mkdir(exist_ok=True) all_df.to_parquet(cache_path, index=False) return all_df提示:
pandas-ta的macd()函数默认返回MACD_12_26_9,MACDh_12_26_9,MACDs_12_26_9三列,框架在compute_batch后自动重命名为macd_line,macd_signal,macd_hist,与国内主流软件命名对齐——这种细节正是高分作业的体现。
3.3 自定义指标开发模板:“量能饱和度圆圈1.00”的实现逻辑
“量能饱和度圆圈”是 A 股常用情绪指标,定义为:(当日成交量 / 近 N 日平均成交量) * 100,当值 > 150 时显示红色圆圈。框架提供indicators/saturation.py作为扩展入口:
# indicators/saturation.py import pandas as pd import numpy as np def saturation_circle(df: pd.DataFrame, window: int = 20) -> pd.DataFrame: """ 计算量能饱和度圆圈指标 :param df: 包含 'volume' 列的 DataFrame :param window: 均值窗口期(默认20日) :return: 添加 'saturation_circle' 列的 DataFrame """ # 计算滚动均值,使用 'min_periods=1' 处理起始不足 window 日的情况 avg_vol = df["volume"].rolling(window=window, min_periods=1).mean() # 饱和度 = 当日量 / 近 window 日均量 * 100 df["saturation_circle"] = (df["volume"] / avg_vol * 100).round(2) # 标记状态:>150 为高饱和(红圈),<80 为低饱和(绿圈) df["saturation_status"] = "normal" df.loc[df["saturation_circle"] > 150, "saturation_status"] = "high" df.loc[df["saturation_circle"] < 80, "saturation_status"] = "low" return df该函数可直接被IndicatorEngine加载,无需编译或安装额外包。学生修改window参数或阈值后,重新运行pipeline.py即可生成新指标——这比修改talibC 源码现实得多。
4. 使用 Plotly + Jinja2 构建交互式可视化报告:脱离 notebook 的独立 HTML 输出
4.1 为什么不用 matplotlib?Plotly 的动态交互与导出能力
matplotlib生成静态 PNG,无法缩放查看 K 线细节;seaborn侧重统计分布,不擅长金融时序叠加图。Plotly支持:
- 鼠标悬停显示精确价格与日期
- 滚轮缩放任意时间段
- 右键拖拽平移
- 导出为 SVG 矢量图(论文插图需求)
- 嵌入 HTML 时自动适配移动端
框架将 K 线图、指标子图、成交量柱状图封装为ChartBuilder类,每个图表均启用mode_bar_additions添加“下载为 PNG”按钮:
# viz/chart_builder.py import plotly.graph_objects as go from plotly.subplots import make_subplots class ChartBuilder: def build_kline_chart(self, df: pd.DataFrame, symbol: str) -> go.Figure: fig = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05, subplot_titles=("K线图", "MACD", "成交量"), specs=[[{"type": "candlestick"}], [{"type": "scatter"}], [{"type": "bar"}]] ) # K线主图 fig.add_trace( go.Candlestick( x=df["trade_date"], open=df["open"], high=df["high"], low=df["low"], close=df["close"], name="K线" ), row=1, col=1 ) # MACD 子图 fig.add_trace( go.Scatter(x=df["trade_date"], y=df["macd_line"], mode="lines", name="MACD线"), row=2, col=1 ) fig.add_trace( go.Scatter(x=df["trade_date"], y=df["macd_signal"], mode="lines", name="信号线"), row=2, col=1 ) fig.add_trace( go.Bar(x=df["trade_date"], y=df["macd_hist"], name="MACD柱"), row=2, col=1 ) # 成交量柱 fig.add_trace( go.Bar(x=df["trade_date"], y=df["volume"], name="成交量"), row=3, col=1 ) # 配置布局 fig.update_layout( title=f"{symbol} 股票分析报告({df['trade_date'].min()} 至 {df['trade_date'].max()})", height=800, showlegend=True, mode_bar_additions=["v1image", "zoomIn2d", "zoomOut2d", "autoScale2d", "downloadImage"], xaxis_rangeslider_visible=False # 关闭底部缩略图,用滚轮替代 ) return fig4.2 Jinja2 模板驱动报告生成:插入指标统计摘要与风险提示
HTML 报告非简单图表堆砌,而是结构化文档。框架使用templates/report.html作为 Jinja2 模板,动态注入:
summary_stats:df.describe()提取的close列均值、标准差、最大回撤risk_alerts: 标记出saturation_status == "high"且rsi > 70的日期(超买预警)indicator_table: 指标参数配置表(来自indicators.yaml)
<!-- templates/report.html --> <h2>📊 指标参数配置</h2> <table border="1" class="dataframe"> <thead><tr><th>指标</th><th>参数</th></tr></thead> <tbody> {% for name, config in indicators.items() %} <tr> <td>{{ name }}</td> <td>{{ config.params|tojson }}</td> </tr> {% endfor %} </tbody> </table> <h2>⚠️ 风险提示</h2> <ul> {% for alert in risk_alerts %} <li>{{ alert.date }}:{{ alert.message }}(RSI={{ alert.rsi }},饱和度={{ alert.saturation }})</li> {% endfor %} </ul>生成命令python report_generator.py --symbol 000001.SZ --start 20240101 --end 20240630输出reports/000001.SZ_20240101_20240630.html,双击即可在浏览器查看完整交互报告——这比提交.ipynb文件更符合企业交付规范。
4.3 响应式布局与离线资源:确保无网络环境可查看
Plotly 默认从 CDN 加载 JavaScript,导致离线打不开。框架在report_generator.py中启用include_plotlyjs="cdn"→"require",并配合plotly.offline.plot()导出内联 JS:
# report_generator.py def generate_html_report(fig: go.Figure, output_path: str): # 导出为包含完整 JS 的 HTML(约 3MB),离线可用 fig.write_html( output_path, include_plotlyjs="require", # 内联 plotly.js full_html=True, config={"displayModeBar": True} ) logger.info(f"Report saved to {output_path}")同时,templates/base.html中<head>部分预置字体加载失败降级方案:
<head> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" onerror="this.href='static/fonts.css'"> </head>当网络不可用时,自动加载本地static/fonts.css(含@font-face定义),保证中文不显示方块——这是金融类报告的基本体验底线。
5. 实战技巧:三步完成一只股票的全流程分析(含参数速查表)
5.1 一条命令启动全链路:从数据获取到 HTML 报告生成
假设你要分析贵州茅台(600519.SH)2024年第二季度行情,只需执行:
# 第一步:安装依赖(仅首次) pip install -r requirements.txt # 第二步:配置股票列表(编辑 config/symbols.txt) echo "600519.SH" > config/symbols.txt # 第三步:运行端到端流水线(自动完成爬取→清洗→计算→绘图→生成HTML) python main.py --start-date 20240401 --end-date 20240630 --symbols-file config/symbols.txtmain.py内部调用顺序为:DataPipeline.run()→IndicatorEngine.compute_batch()→ChartBuilder.build_kline_chart()→report_generator.generate_html_report()。全程无交互,输出日志清晰标注各阶段耗时:
INFO:root:Fetching data for 600519.SH (2024-04-01 to 2024-06-30)... INFO:root:Loaded 62 rows from cache/stock/600519.SH_20240401_20240630.parquet INFO:root:Computing indicators... (macd, rsi, saturation_circle) INFO:root:Generated chart for 600519.SH (height=800px) INFO:root:Report saved to reports/600519.SH_20240401_20240630.html提示:
--symbols-file参数支持多只股票批量分析,config/symbols.txt每行一个代码,框架会自动并行处理(concurrent.futures.ThreadPoolExecutor),10只股票耗时仅比单只多 1.2 倍,而非 10 倍。
5.2 关键参数速查表:修改哪里能改变什么结果
| 配置文件 | 参数路径 | 默认值 | 修改影响 | 适用场景 |
|---|---|---|---|---|
config/indicators.yaml | macd.params.fast | 12 | MACD 快线周期,值越小越敏感 | 短线交易者调至8 |
config/indicators.yaml | rsi.params.length | 14 | RSI 计算周期,值越大越平滑 | 长线投资者调至21 |
config/pipeline.yaml | cleaner.remove_invalid_zero_volume | true | 是否删除无效零成交量行 | 研究 ST 股票时设为false |
config/viz.yaml | chart.height | 800 | HTML 报告总高度(像素) | 屏幕分辨率低时设为600 |
config/db.yaml | sqlite.path | data/stock.db | SQLite 数据库存储路径 | 需长期保存时指向 SSD 盘 |
所有参数均通过omegaconf加载,支持 YAML 注释(#开头行),修改后无需重启 Python 进程,下次运行自动生效。
5.3 验证分析结果准确性的三个必做动作
高分项目必须经得起推敲。交付前请执行以下验证:
- 比对权威源:打开东方财富网(
https://quote.eastmoney.com/sh600519.html),截图“K线图”与报告中600519.SH_20240401_20240630.html的 2024-05-10 日 K 线,确认open/high/low/close/volume数值完全一致; - 检查指标逻辑:在报告 HTML 中右键 → “查看页面源代码”,搜索
macd_line,找到某日数值(如 2024-05-10 的macd_line=12.34),用 Excel 手动计算EMA(close,12)与EMA(close,26),验证差值是否匹配; - 测试边界情况:将
config/symbols.txt改为000001.SZ(平安银行),运行python main.py --start-date 20200101 --end-date 20200101,确认程序能正确处理单日数据(无KeyError或IndexError)。
这三个动作覆盖了数据源一致性、算法正确性、鲁棒性三重验证,是答辩时老师最可能追问的环节。
本文还有配套的精品资源,点击获取