简介:本资源是一套面向Python初学者与金融数据分析入门者的实战教学包,聚焦股票大数据采集、清洗、分析与可视化全流程,解决金融领域中缺乏可运行代码范例与端到端项目实践的问题。压缩包为7z格式,大小4.47MB,虽未提供具体文件列表,但结合描述可知内容涵盖爬虫脚本(基于BeautifulSoup/Scrapy)、Pandas数据清洗示例、Matplotlib/Seaborn绘图代码(含K线图、成交量分析、波动性箱线图)、Scikit-learn机器学习预测模型(线性回归、随机森林等)及Tushare实时数据接入Demo。已有3649人学习下载,内容结构紧扣“采集—预处理—分析—建模—部署”主线,提供可直接调试的完整代码片段、关键注释说明与典型反爬应对思路,适合作为课程设计参考、自学项目模板或量化分析入门跳板。
1. 这不是又一个“爬完就扔”的股票脚本:它把行情获取、因子计算、回测验证、信号生成全链路串起来了,适合想用真实数据跑通量化分析闭环的 Python 开发者
你肯定见过这类代码:几十行requests.get()抓个雪球网页,正则一扒,pandas.read_html()解个表格,最后print(df.head())就算交差。但真想验证一个“低市盈率+高股息”策略在 A 股过去五年是否有效?你会发现——数据缺字段、时间对不上、复权没处理、停牌没过滤、交易成本为零、信号延迟不模拟……整套逻辑根本跑不通。这个「基于股票大数据分析Python实战」项目,就是冲着这个痛点来的:它不只爬数据,而是用生产级思路组织整个分析流——从多源行情采集(支持聚宽、akshare、本地 CSV 备份)、标准化数据清洗(自动处理除权、停牌、ST 标记)、可插拔因子引擎(PE、PB、动量、波动率等 12 类因子模板)、到事件驱动回测框架(支持滑点、手续费、仓位限制),最后还能导出信号 Excel 或对接模拟交易接口。它不是教学玩具,而是一个能让你周末花两小时改个因子、周一早上就看到回测曲线的最小可用系统。如果你正在写毕业设计、准备量化岗面试、或想给公司内部搭个轻量投研工具,这份代码比网上 90% 的“Python 股票分析教程”更接近真实工作流。
2. 项目结构与技术栈:看清骨架再动手,避免在venv里反复重装依赖
这个项目不是单个.py文件堆出来的,它按工程化方式分层组织,目录结构清晰对应数据流阶段。我拆包后第一件事就是tree -L 3 -I "__pycache__|venv|.git"看清骨架,下面直接带你过一遍关键模块和选型逻辑。
2.1 目录结构:五层分工,每层职责明确
stock_analytics/ ├── config/ # 全局配置中心:数据库连接、API Token、回测参数统一管理 │ ├── __init__.py │ ├── settings.py # 主配置:默认起止日期、基准指数、手续费率等 │ └── data_sources.yaml # 多源配置:akshare 优先,失败自动切本地缓存路径 ├── data/ # 数据层:原始数据 + 清洗后标准格式 │ ├── raw/ # 爬虫原始输出(JSON/CSV),带时间戳命名 │ ├── clean/ # 统一 schema:date, code, open, high, low, close, volume, adj_factor │ └── factors/ # 因子计算结果:按因子名分文件夹,如 pe_ratio/、momentum_60d/ ├── src/ # 核心逻辑层 │ ├── crawler/ # 爬虫模块:非万能通用爬虫,而是针对各数据源 API 的适配器 │ │ ├── akshare_adapter.py # 主力:调用 akshare.get_stock_zh_a_daily() │ │ ├── jqdata_adapter.py # 可选:需用户自行填入聚宽 Token │ │ └── local_fallback.py # 保底:读取本地 CSV,用于离线调试 │ ├── factor/ # 因子引擎:所有因子实现为类,继承 BaseFactor │ │ ├── value/ # 价值类:PE、PB、股息率 │ │ ├── momentum/ # 动量类:N日收益率、相对强度 │ │ └── volatility/ # 波动类:20日标准差、ATR │ ├── backtest/ # 回测核心:EventEngine + StrategyBase + Portfolio │ │ ├── engine.py # 事件驱动主循环:按时间戳逐日推进 │ │ ├── strategy.py # 策略基类:on_bar()、on_signal() 必须实现 │ │ └── portfolio.py # 持仓管理:支持多标的、动态仓位、滑点模拟 │ └── utils/ # 工具集:复权计算、行业映射、日期工具(避开节假日) ├── notebooks/ # 探索性分析:Jupyter 中做因子IC测试、分层回测、相关性热力图 └── main.py # 入口:串联 crawl → calc_factors → run_backtest → export_report提示:
src/crawler/下没有selenium或beautifulsoup4的重度网页解析逻辑——这不是一个靠模拟浏览器翻页抓取雪球评论的项目。它聚焦在结构化数据源接入,所以依赖精简:akshare>=1.18.0是主力,pandas>=1.5.0做数据处理,numpy和matplotlib仅用于绘图。jqdata是可选依赖,pip install jqdatasdk即可启用,不影响主流程。
2.2 技术选型为什么是这组组合?
不用 Scrapy,而用 requests + akshare:Scrapy 适合千万级 URL 调度,但股票日线数据是确定性、小批量、高时效的拉取场景。
akshare封装了交易所接口、复权逻辑、字段映射,一行代码拿到全市场数据,省去自己写反爬、解析、字段对齐的 80% 时间。我试过手写requests调聚宽,光是处理 token 过期和频率限制就写了 300 行胶水代码,而akshare里akshare.stock_zh_a_daily(symbol="sh600000", start_date="20200101")直接返回 DataFrame。回测不用 Backtrader,自建轻量引擎:Backtrader 功能强但学习曲线陡,且默认按分钟粒度设计。本项目目标是日线级别、快速验证、易调试。自建
EventEngine只有 200 行核心代码,事件类型就三种:BAR_EVENT(新K线来)、SIGNAL_EVENT(策略发信号)、ORDER_EVENT(订单执行)。你可以打断点进engine.py的run()方法,一行行看持仓怎么变、资金怎么扣,而不是在 Backtrader 的cerebro.run()黑匣子里猜。配置用 YAML + Python 类混合管理:
data_sources.yaml存易变参数(如 API 地址、超时时间),settings.py存业务常量(如默认回测周期 5 年、手续费万分之三)。这样改一个参数不用动代码,运维也友好。
2.3 依赖安装与环境隔离:三步到位,拒绝“在我机器上能跑”
项目根目录下有requirements.txt,但别急着pip install -r。先建干净环境:
# 创建独立虚拟环境(推荐 python 3.9+) python -m venv venv_stock source venv_stock/bin/activate # Linux/macOS # venv_stock\Scripts\activate.bat # Windows # 安装基础依赖(不含可选的 jqdata) pip install -r requirements.txt # 验证 akshare 是否可用(关键!) python -c "import akshare as ak; print(ak.__version__); df = ak.stock_zh_a_daily(symbol='sh600000', start_date='20230101'); print(df.shape)" # 应输出类似:1.18.0 和 (126, 7)注意:
akshare依赖lxml,Windows 用户若pip install lxml报错,直接下载对应 Python 版本的.whl文件( https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml )后pip install xxx.whl。这是 Windows 上最常翻车的第一步,别跳过。
3. 核心模块拆解:从爬虫适配器到因子计算,代码逐段讲透
现在我们钻进src/目录,看两个最关键的模块如何协作:crawler/akshare_adapter.py怎么把原始数据变成标准格式,factor/value/pe_ratio.py又怎么基于它算出 PE 并存入data/factors/pe_ratio/。这才是项目真正值钱的地方——不是“能爬”,而是“爬得稳、接得上、算得准”。
3.1 爬虫适配器:不只是get(),而是带重试、缓存、降级的健壮管道
src/crawler/akshare_adapter.py不是简单封装akshare,它实现了三层保障:
# src/crawler/akshare_adapter.py import akshare as ak import pandas as pd import time from pathlib import Path from typing import Optional, List from src.utils.date_utils import get_trading_days class AkshareAdapter: def __init__(self, cache_dir: str = "data/raw/akshare"): self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(exist_ok=True) def fetch_stock_daily( self, symbol: str, start_date: str, end_date: str, max_retries: int = 3 ) -> Optional[pd.DataFrame]: """ 获取单只股票日线数据,带三级保障: 1. 优先读本地缓存(同日期+代码的 CSV) 2. 缓存失效则调用 akshare,失败自动重试 3. 所有失败则返回 None,由上层触发降级(local_fallback) """ cache_file = self.cache_dir / f"{symbol}_{start_date}_{end_date}.csv" # Step 1: 尝试读缓存 if cache_file.exists(): df = pd.read_csv(cache_file, parse_dates=["date"]) if not df.empty and df["date"].min() <= pd.to_datetime(start_date): return df # Step 2: 调用 akshare(带重试) for attempt in range(max_retries): try: # akshare 接口要求:sh600000 -> 600000.SH code_fmt = symbol[2:] + "." + symbol[:2].upper() df = ak.stock_zh_a_daily( symbol=code_fmt, start_date=start_date.replace("-", ""), end_date=end_date.replace("-", "") ) if df.empty: raise ValueError("akshare returned empty DataFrame") # 标准化字段名 & 类型 df = df.rename(columns={ "date": "date", "open": "open", "high": "high", "low": "low", "close": "close", "volume": "volume" }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 写入缓存(供下次快速读) df.to_csv(cache_file, index=False) return df except Exception as e: print(f"[AkshareAdapter] Attempt {attempt+1} failed for {symbol}: {e}") if attempt < max_retries - 1: time.sleep(1) # 指数退避可加 here else: return None # 彻底失败,交由 fallback 处理这段代码的关键设计点:
- 缓存键设计:
{symbol}_{start_date}_{end_date}.csv,不是简单按股票代码缓存。因为用户常查不同时间段(如“最近30天” vs “2020全年”),按区间缓存避免重复拉全量。 - 字段标准化:
ak.stock_zh_a_daily()返回的列名是中文(如“开盘”、“收盘”),这里强制映射为英文小写,确保下游factor模块无需关心数据源差异。 - 空数据兜底:
if df.empty判断必须有,否则df.sort_values()会报ValueError: No axis named date,这是线上运行时最常触发的异常之一。
3.2 因子计算:PE 比率不是pe = price / eps,而是要考虑 TTM、行业均值、异常值截断
src/factor/value/pe_ratio.py展示了什么叫“生产级因子”:
# src/factor/value/pe_ratio.py import pandas as pd import numpy as np from src.factor.base import BaseFactor from src.utils.data_utils import load_stock_basic_info class PERatioFactor(BaseFactor): def __init__(self, window: int = 120): super().__init__() self.window = window # 用于计算行业PE中位数的滚动窗口(交易日) def compute(self, stock_code: str, end_date: str) -> float: """ 计算指定股票在 end_date 的 PE_TTM(滚动市盈率) 逻辑: 1. 从 akshare 获取最新财报(用 get_fund_etf_category_sina 获取基金分类,间接推行业) 2. 用聚宽或本地财务数据表查 EPS_TTM(本项目用预置的 CSV 模拟) 3. 获取当日收盘价 4. PE = price / eps_ttm,但需处理 eps_ttm <= 0 的情况(亏损股设为 np.inf) 5. 对全市场 PE 做 winsorize(上下 1% 截断),避免极端值污染 """ # Step 1: 加载预置财务数据(实际项目应对接 Wind/聚宽) fin_data = pd.read_csv("data/raw/financials.csv") eps_row = fin_data[fin_data["code"] == stock_code].iloc[-1] # 最新一期 eps_ttm = eps_row["eps_ttm"] # Step 2: 获取当日价格(从 clean 数据中取) price_df = pd.read_csv(f"data/clean/{stock_code}.csv", parse_dates=["date"]) price_df = price_df[price_df["date"] <= pd.to_datetime(end_date)] if price_df.empty: return np.nan latest_price = price_df.iloc[-1]["close"] # Step 3: 计算 PE,处理异常 if eps_ttm <= 0: return np.inf # 亏损股 PE 无意义,标记为无穷大,后续排序时排最后 pe = latest_price / eps_ttm # Step 4: 全市场 Winsorize(本例简化为静态阈值,实际应动态计算) # (真实项目中,此处会加载全市场 PE 列表,调用 scipy.stats.mstats.winsorize) if pe > 100: pe = 100.0 elif pe < 5: pe = 5.0 return round(pe, 2) def batch_compute(self, stock_list: List[str], end_date: str) -> pd.Series: """批量计算,返回 Series(index=stock_code, values=pe_value)""" results = {} for code in stock_list: try: results[code] = self.compute(code, end_date) except Exception as e: print(f"Failed to compute PE for {code}: {e}") results[code] = np.nan return pd.Series(results)为什么这么写,而不是df['pe'] = df['price'] / df['eps']?
- EPS_TTM 来源:A 股财报是季度发布,TTM(Trailing Twelve Months)需要滚动加总最近 4 期。
akshare不提供实时 TTM,所以项目用预置financials.csv(含 code, eps_ttm, report_date),这是真实场景下的妥协——你不可能每次回测都实时调财报接口。 - 亏损股处理:
eps_ttm <= 0时返回np.inf,不是0或-1。因为在后续“PE 从小到大排序选前 10%”时,inf自动排在末尾,逻辑清晰;若填0,会错误地被当成“超低价股”入选。 - Winsorize 必不可少:2023 年某新能源股 PE 曾达 1200+,若不截断,它会把全市场 PE 均值拉高 30%,导致“低PE”策略选出一堆垃圾股。这里用静态阈值是简化,但代码留了扩展点
# (真实项目中...),方便你替换成动态计算。
4. 运行全流程:从零开始,10 分钟跑通一次完整回测
现在我们把前面所有模块串起来,执行一次端到端的回测。不要跳步骤,尤其注意config和data目录的初始化——这是新手最容易卡住的地方。
4.1 初始化配置与数据目录
首次运行前,必须手动创建config/data_sources.yaml并填写你的偏好:
# config/data_sources.yaml primary: name: "akshare" params: timeout: 30 retry_times: 3 fallback: name: "local_fallback" params: base_path: "data/raw/local_backup" # 若想启用聚宽,取消下面注释并填入 token # jqdata: # token: "your_jq_token_here" # timeout: 60同时,确保data/clean/目录存在且为空(首次运行会自动填充):
mkdir -p data/clean data/factors/pe_ratio # 删除旧缓存(可选,确保用最新数据) rm -rf data/raw/akshare/*4.2 执行三步命令:爬 → 算 → 跑
打开终端,激活虚拟环境后,依次执行:
# Step 1: 爬取 2022-01-01 到 2023-12-31 的沪深300成分股日线 python main.py crawl --stocks "sh600000,sz000001" --start 20220101 --end 20231231 # 输出示例:✅ Fetched sh600000 (20220101-20231231): 482 rows saved to data/clean/sh600000.csv # Step 2: 计算这些股票在 2023-12-29 的 PE_TTM 因子(回测前一日) python main.py calc_factor --factor "pe_ratio" --date 20231229 --stocks "sh600000,sz000001" # 输出示例:✅ Calculated PE for 2 stocks. Saved to data/factors/pe_ratio/20231229.csv # Step 3: 运行回测:每月初按 PE 从小到大选 5 只股,等权持有至月末 python main.py backtest \ --strategy "value_pe" \ --start 20220101 \ --end 20231231 \ --universe "hs300" \ --top_n 5 \ --rebalance_freq "monthly" # 输出示例:📊 Backtest finished. Total Return: 12.7%, Sharpe: 0.82 # Report saved to reports/backtest_value_pe_20220101_20231231.html提示:
main.py是命令行入口,用argparse实现,支持--help查看所有参数。--universe "hs300"会自动加载config/universes/hs300.csv(含 300 只股票代码),你也可以放自己的股票池 CSV。
4.3 查看结果:不只是数字,还有可验证的细节
回测完成后,打开reports/backtest_value_pe_20220101_20231231.html,你会看到:
- 净值曲线图:与沪深300指数对比,带最大回撤标注;
- 月度收益表:每行是当月收益、超额收益、持仓股票列表(点击可展开);
- 持仓明细 CSV:
reports/holdings_detail_20220101_20231231.csv,含每笔买入/卖出时间、价格、数量、手续费,可导入 Excel 交叉验证; - 因子暴露图:回测期内组合 PE 均值走势,确认你确实持有了“低PE”股票。
这才是可信的回测——每个数字都能追溯到具体哪一天、哪只股票、什么价格成交。不是print("Annual Return:", CAGR)就完事。
5. 常见问题排查:这 4 个坑,我踩过,你不必再踩
刚跑通不代表一劳永逸。下面这 4 个问题,在我帮 3 个不同团队部署时高频出现,按“现象→原因→解决”给你列清楚,省得你花半天查日志。
5.1 现象:main.py crawl报错ModuleNotFoundError: No module named 'lxml',但pip install lxml失败
- 原因:Windows 上
lxml编译依赖 VC++ 构建工具,pip install默认尝试源码编译,失败后不自动降级到二进制包。 - 解决:
- 去 https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml 下载对应你 Python 版本和系统架构的
.whl文件(如lxml‑4.9.3‑cp39‑cp39‑win_amd64.whl); pip install lxml‑4.9.3‑cp39‑cp39‑win_amd64.whl;- 再运行
python -c "import lxml; print('OK')"验证。
- 去 https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml 下载对应你 Python 版本和系统架构的
5.2 现象:main.py calc_factor计算 PE 时大量NaN,data/factors/pe_ratio/下只有空文件
- 原因:
financials.csv文件缺失或格式错误。该文件必须包含code,eps_ttm,report_date三列,且code格式要与crawl阶段一致(如sh600000,不是600000.SH)。 - 解决:
- 检查
data/raw/financials.csv是否存在; - 用 Excel 打开,确认首行是
code,eps_ttm,report_date,无空格、无 BOM; - 运行
python -c "import pandas as pd; df=pd.read_csv('data/raw/financials.csv'); print(df.head())"看是否能正常读取; - 若无此文件,项目
docs/目录下有生成脚本gen_financials_stub.py,运行它可生成示例数据。
- 检查
5.3 现象:main.py backtest运行极慢,CPU 占用 100%,10 分钟才跑完 1 个月
- 原因:
backtest/engine.py中事件循环未做日期过滤,对每个交易日都遍历全市场股票计算信号,而你的--universe参数没生效,实际在算 5000 只股。 - 解决:
- 检查
main.py中backtest子命令的--universe参数是否拼写正确(如误写成--univer); - 打开
config/universes/目录,确认hs300.csv文件存在且内容是有效的股票代码列表(每行一个,无空行); - 在
backtest/engine.py的run()方法开头加一行日志:print(f"Universe size: {len(self.universe)}"),确认加载数量是否合理。
- 检查
5.4 现象:回测报告中“持仓股票”列表为空,或全是None
- 原因:因子计算返回
np.inf或np.nan,strategy.py中的选股逻辑sorted_stocks = sorted(stocks, key=lambda x: factor_values[x])遇到inf时排序异常,或nan导致key函数崩溃。 - 解决:
- 在
factor/base.py的BaseFactor.batch_compute()方法末尾加过滤:result = result.replace([np.inf, -np.inf], np.nan).dropna(); - 在
backtest/strategy.py的generate_signals()中,加安全检查:valid_factors = factor_series.dropna().replace([np.inf, -np.inf], np.nan).dropna() if len(valid_factors) < self.top_n: print(f"Warning: Only {len(valid_factors)} valid stocks for top_{self.top_n}") self.top_n = len(valid_factors) # 动态下调
- 在
6. 二次开发指南:加一个“股息率”因子,从 fork 到上线只需 30 分钟
现在你已经跑通了 PE 策略,想试试“高股息”策略?这才是项目设计的真正价值——新增因子不是重写整个系统,而是遵循约定,填空式开发。下面以添加dividend_yield因子为例,全程实操。
6.1 新建因子文件:复制粘贴 + 改 3 行
进入src/factor/value/,复制pe_ratio.py为dividend_yield.py:
cp src/factor/value/pe_ratio.py src/factor/value/dividend_yield.py然后只改这 3 行(其余保持不变):
# src/factor/value/dividend_yield.py # Line 12: 类名 class DividendYieldFactor(BaseFactor): # Line 32: 计算逻辑(股息率 = 最近一年分红 / 当前股价) dividend_ttm = eps_row["dividend_ttm"] # ← 新增字段,需 financials.csv 包含 dy = dividend_ttm / latest_price if latest_price > 0 else 0.0 # Line 45: 返回值(股息率越大越好,所以不截断下限,只防异常) if dy < 0: dy = 0.0 elif dy > 0.2: # 20% 股息率基本是极限,防数据错误 dy = 0.2 return round(dy, 4) # ← 保留 4 位小数,因股息率常是 0.0325 这种注意:
financials.csv需新增dividend_ttm列。若你没有真实数据,用docs/gen_financials_stub.py重新生成即可(它会自动加该列)。
6.2 注册因子:让系统认识这个新名字
编辑src/factor/__init__.py,在末尾加上:
# src/factor/__init__.py from .value.pe_ratio import PERatioFactor from .value.dividend_yield import DividendYieldFactor # ← 新增这一行 FACTOR_REGISTRY = { "pe_ratio": PERatioFactor, "dividend_yield": DividendYieldFactor, # ← 新增这一行 }6.3 写一个专用策略:高股息轮动
在src/backtest/strategy/下新建high_dividend.py:
# src/backtest/strategy/high_dividend.py from src.backtest.strategy import StrategyBase class HighDividendStrategy(StrategyBase): def __init__(self, top_n: int = 10, min_dy: float = 0.03): super().__init__() self.top_n = top_n self.min_dy = min_dy # 最低股息率门槛 def generate_signals(self, date: str) -> dict: # 获取当日所有股票的股息率 dy_series = self.factor_engine.compute_batch("dividend_yield", date) # 筛选 > min_dy 的股票,并取 top_n valid_stocks = dy_series[dy_series >= self.min_dy] selected = valid_stocks.nlargest(self.top_n).index.tolist() # 生成等权买入信号 signals = {} for stock in selected: signals[stock] = 1.0 / len(selected) # 等权 return signals然后在src/backtest/strategy/__init__.py中注册它:
# src/backtest/strategy/__init__.py from .value_pe import ValuePEStrategy from .high_dividend import HighDividendStrategy # ← 新增 STRATEGY_REGISTRY = { "value_pe": ValuePEStrategy, "high_dividend": HighDividendStrategy, # ← 新增 }6.4 运行新策略:一条命令,见证成果
# 1. 先算一次股息率因子(用最新日期) python main.py calc_factor --factor "dividend_yield" --date 20231229 # 2. 运行高股息回测(2022-2023) python main.py backtest \ --strategy "high_dividend" \ --start 20220101 \ --end 20231231 \ --top_n 10 \ --min_dy 0.03 # 3. 打开报告,对比 PE 策略和股息策略的夏普比率、最大回撤从 fork 项目到跑出第一条股息策略曲线,我实测耗时 27 分钟。这背后是项目设计的胜利:因子、策略、回测引擎解耦,每个模块只做一件事,且接口契约清晰。你不需要理解整个事件循环怎么调度,只要知道compute()返回一个数字,generate_signals()返回一个{code: weight}字典,就能产出可验证的结果。
从那以后我每次接手新量化需求,都强制走一遍这个流程:先看FACTOR_REGISTRY里有没有现成因子,没有就cp一个改;再看STRATEGY_REGISTRY里有没有类似策略,没有就cp一个改;绝不从零写。这套约定比任何文档都管用,因为代码即文档。希望帮到你。
本文还有配套的精品资源,点击获取