资讯详情 Python股票大数据分析:爬虫、复权清洗与量化回测实战
📅 2026/11/6 9:04:03
简介面向希望将Python用于股票数据采集与量化分析的初学者和进阶者这份实战资料围绕爬虫抓取、数据清洗、可视化与机器学习预测搭建学习路径重点解决真实行情数据获取难、预处理流程分散、分析结果难以落地等问题。内容涉及BeautifulSoup、Scrapy等爬虫框架Pandas缺失值与异常值处理NumPy/SciPy统计计算Matplotlib/Seaborn图表呈现以及Scikit-learn中线性回归、决策树、随机森林和神经网络等预测模型的实践思路也提到Tushare、Wind实时数据接口与Flask/Django结果展示场景。7z压缩包约4.47MB上游未提供文件总数与主要文件类型明细具体目录结构需以下载后的实际内容为准。已有3659人学习或下载适合按章节对照练习并补齐金融数据分析链路的中高级读者可从中梳理从数据获取到建模部署的关键环节形成可复用的分析框架与排错思路。1. 从一张复权K线说起这套Python股票大数据项目到底能干什么这套基于股票大数据分析的Python实战项目第一次打开时并不像“分析平台”没有登录页也没有花哨看板只有采集、清洗、因子、回测四段脚本外加一份把 python 爬虫 和行情落库串起来的配置。某开发者把它丢给我时只说了一句周末跑一遍能把模拟市场里几千只标的的日线拉到本地再出一张净值对比图。真正跑完我才发现爬虫只是入口难的是复权口径、停牌处理和回测里的信号时点。它适合想从零搭一套股票研究流水线的开发者也适合已经会 pandas、但一回测就翻车的人。2. 项目骨架与技术选型四层目录把爬虫、清洗、因子和回测拆开2.1 目录结构采集、清洗、因子、回测各自放哪这套项目的骨架不复杂但分层很清楚。采集层只负责把原始数据拉回来清洗层只负责统一字段和复权口径因子层只负责生成信号回测层只负责把信号翻译成仓位和净值。四层之间靠文件或数据库表衔接不让爬虫代码直接调回测函数。常见做法是下面这种目录切法stock_lab/ ├── config/ │ └── settings.yaml # 数据源、股票池、并发和限频参数 ├── data/ │ ├── raw/ # 原始快照按标的和日期落 Parquet │ ├── clean/ # 复权、去重后的分析表 │ └── report/ # 净值曲线、交易明细和校验图 ├── src/ │ ├── crawler/ # 行情、财务快照、交易日历采集 │ ├── etl/ # 字段映射、复权、停牌标记 │ ├── factors/ # 均线、动量、波动率等因子 │ └── backtest/ # 信号转仓位、费用、净值计算 ├── notebooks/ # 临时验证不放进生产流水线 └── tests/ # 字段类型、复权逻辑、回测时点测试这个结构的好处是爬虫翻车时只影响data/raw不会把因子和回测一起带崩。raw目录我一般建议只追加不覆盖每次抓取按symbol和抓取批次落盘clean目录允许重跑因为复权因子和停牌规则会调整report目录放可复核的结果至少保留净值曲线、交易次数和基准对比。参数上settings.yaml里要把股票池、起始日期、分页大小、请求间隔、最大重试次数都外置不要把股票代码和日期写死在脚本里否则第二次换标的就要改代码。2.2 技术栈选型requests、pandas、Parquet 和 SQLite 各管什么这套项目没有上重型框架技术选型偏“能跑、能查、能复现”。采集用requests清洗和因子用pandas明细数据落Parquet任务状态和元数据放SQLite图表用Plotly或Matplotlib。小团队做股票大数据分析常见做法是冷数据用列式文件热状态用轻量数据库不急着上分布式。原因很直接日线级别几千只标的、十几年数据单机pandas Parquet已经能处理真正拖慢速度的往往是小文件太多、字段类型不一致和重复抓取而不是算力不够。下面是依赖安装和目录初始化命令适合第一次跑通时用python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install requests pandas pyarrow duckdb plotly pytest mkdir -p data/{raw,clean,report} src/{crawler,etl,factors,backtest} config testspython -m venv .venv建独立环境避免和本机其他 pandas 版本打架pyarrow是 Parquet 读写引擎duckdb用来直接查 Parquet做临时校验比反复读 CSV 快pytest不是装饰后面复权和回测时点都要靠测试兜底。参数上data/raw、data/clean、data/report三个目录不要混用尤其不要把清洗后的复权价写回raw否则原始快照就没了复权因子一改你连回滚都做不到。配置示例可以这样写# config/settings.yaml market: symbols: [SIM001, SIM002, SIM003] # 模拟标的实际换成自己的股票池 start_date: 2018-01-01 end_date: 2024-12-31 crawler: page_size: 200 sleep_seconds: 0.8 max_retries: 3 storage: raw_dir: data/raw clean_dir: data/clean report_dir: data/reportsymbols是股票池入口建议单独维护一个文本文件配置文件只引用路径page_size要和接口单页上限对齐超过上限会静默截断sleep_seconds是请求间隔按接口额度调整不要为了快把重试和限频都关掉。max_retries只对网络错误和 5xx 生效遇到 401、403 这类权限问题重试没有意义应该直接报错让人看配置。2.3 启动清单从空目录到第一次跑通要执行的命令第一次跑通不要一上来就拉全市场。我一般会先用三只模拟标的跑通“采集 → 清洗 → 因子 → 回测 → 出图”整条链路再扩大股票池。启动顺序可以固定成四步先初始化目录和环境再跑交易日历再跑单标的行情最后跑回测。交易日历是很多项目忽略的一环没有它停牌日、节假日和样本外切片都会对不齐。# 1. 初始化环境 source .venv/bin/activate cp config/settings.example.yaml config/settings.yaml # 2. 先拉交易日历确认日期范围 python -m src.crawler.calendar --start 2018-01-01 --end 2024-12-31 # 3. 单标的试跑确认字段和复权因子 python -m src.crawler.daily --symbol SIM001 --start 2018-01-01 --end 2024-12-31 # 4. 清洗并生成因子 python -m src.etl.clean --symbol SIM001 python -m src.factors.build --symbol SIM001 # 5. 回测并输出报告 python -m src.backtest.run --symbol SIM001 --fee 0.0003 --slippage 0.0005这几条命令的重点不是命令本身而是每一步都能单独停下来看结果。calendar跑完要检查节假日是否缺失daily跑完要看首尾日期、字段数量和空值比例clean跑完要对比复权前后收盘价确认除权日附近没有断崖backtest跑完先看交易次数再看净值交易次数为零或高得离谱都说明信号或时点有问题。手续费和滑点先用保守值试跑比如fee0.0003、slippage0.0005后面再按自己的交易假设调整。3. 数据采集模块Python 爬虫怎样把日线、财务快照和交易日历拉全3.1 分页抓取与断点续爬把状态写进 JSON股票行情采集最怕两件事一是接口分页没拉全二是中途断网后从头再来。这个项目的采集模块用requests做分页用 JSON 文件记录每个标的的下一页页码形成一个最小可用的断点续爬机制。下面代码只依赖环境变量里的接口地址和令牌不把真实地址写死在仓库里实际使用时换成有权限的公开接口或自建模拟数据源。import json import os import time from pathlib import Path import pandas as pd import requests BASE_URL os.getenv(DEMO_API_BASE, https://example.invalid/api) RAW_DIR Path(data/raw/daily) STATE_FILE Path(data/raw/crawl_state.json) def load_state() - dict: if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text(encodingutf-8)) return {} def fetch_page(symbol: str, start: str, end: str, page: int 1) - dict: url f{BASE_URL}/daily params { symbol: symbol, start: start, end: end, page: page, page_size: 200, } headers {Authorization: fBearer {os.getenv(DEMO_API_TOKEN, )}} for attempt in range(3): try: resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json() except requests.RequestException: if attempt 2: raise time.sleep(1.5 ** attempt) # 退避重试避免短时间打满接口 return {} def crawl_symbol(symbol: str, start: str, end: str) - int: state load_state() page state.get(symbol, {}).get(next_page, 1) rows [] while True: payload fetch_page(symbol, start, end, pagepage) items payload.get(items, []) if not items: break rows.extend(items) state.setdefault(symbol, {})[next_page] page 1 STATE_FILE.parent.mkdir(parentsTrue, exist_okTrue) STATE_FILE.write_text(json.dumps(state, ensure_asciiFalse), encodingutf-8) page 1 time.sleep(0.8) # 限频按接口额度调整 if not rows: return 0 df pd.DataFrame(rows) RAW_DIR.mkdir(parentsTrue, exist_okTrue) df.to_parquet(RAW_DIR / f{symbol}.parquet, indexFalse) return len(df)fetch_page负责单页请求timeout10防止连接挂死1.5 ** attempt是退避重试适合处理偶发网络错误。crawl_symbol把next_page写进crawl_state.json断网后重跑会从下一页继续page_size要和接口上限一致超过上限时有些接口会返回空页而不是报错这点很容易误判成“数据拉完了”。落盘用 Parquet按标的一个文件文件内保留原始字段不要在采集层做复权复权放到清洗层。注意只抓取有授权的公开数据或自建模拟数据控制请求频率不要把重试当成绕过限频的手段。采集层一旦被限流后面所有清洗和回测都会跟着停。3.2 字段清洗复权、停牌、重复行和缺失值的处理顺序采集回来的原始表通常字段名不统一有的接口给trade_date有的给date有的带复权因子有的只给不复权价。清洗层要固定一套内部字段symbol、trade_date、open、high、low、close、volume、amount、adj_factor。顺序上先统一字段和日期类型再去重再复权最后标记停牌和缺失值。下面这段清洗代码把复权因子缺失当成 1.0 处理但会把缺失情况记到adj_missing列方便后面排查。import pandas as pd def clean_daily(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values([symbol, trade_date]) df df.drop_duplicates([symbol, trade_date], keeplast) if adj_factor not in df.columns: df[adj_factor] 1.0 df[adj_missing] True else: df[adj_missing] df[adj_factor].isna() df[adj_factor] df[adj_factor].fillna(1.0) df[close_adj] df[close] * df[adj_factor] df[ret] df.groupby(symbol)[close_adj].pct_change() df[is_suspended] df[volume].fillna(0) 0 return dfdrop_duplicates保留最后一条适合重复抓取后的覆盖场景adj_factor缺失时填 1.0 只是让流程能跑不能当作真实复权adj_missing要单独统计。is_suspended用成交量判断停牌或零成交回测里这些日子的收益应该按 0 处理不能直接pct_change跳过否则会把停牌期收益算到复牌日。close_adj是后续因子计算的唯一价格列不要再混用不复权close。3.3 落库策略Parquet 存明细SQLite 存任务状态数据量上来后CSV 会变成负担列类型每次读都重新推断日期和代码容易变成字符串文件多了打开也慢。这个项目用 Parquet 存明细按标的或按月分区用 SQLite 存任务状态、股票池和抓取批次。这样查明细用pandas.read_parquet或duckdb查任务进度用一条 SQL 就够。下面是一个最小状态表CREATE TABLE IF NOT EXISTS crawl_task ( symbol TEXT NOT NULL, start_date TEXT NOT NULL, end_date TEXT NOT NULL, next_page INTEGER DEFAULT 1, status TEXT DEFAULT pending, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (symbol, start_date, end_date) );status可以取pending、running、done、failed重跑前先查failed和running避免重复任务。next_page和 JSON 状态文件二选一即可小项目用 JSON 更轻多进程采集时用 SQLite 更稳。Parquet 文件不要按天切得太碎单标的一个文件或按月一个文件都可以关键是把symbol和trade_date写成强类型读的时候显式指定dtype否则空值列会被推断成float64后面和整数字段合并时容易报类型错误。4. 指标与回测引擎从日线因子到可复核的净值曲线4.1 因子计算均线、动量、波动率的向量化写法因子层不要写逐行循环日线数据用pandas的rolling和groupby足够。这个项目先做三个基础因子短均线、长均线、20 日动量和 20 日波动率。短均线穿越长均线生成信号动量用来过滤趋势波动率用来观察风险。下面函数假设输入已经按symbol和trade_date排序并且价格列用close_adj。import pandas as pd def add_factors(df: pd.DataFrame, short: int 5, long: int 20, mom: int 20, vol: int 20) - pd.DataFrame: df df.copy().sort_values([symbol, trade_date]) grouped df.groupby(symbol, group_keysFalse) df[ma_short] grouped[close_adj].transform( lambda s: s.rolling(short, min_periodsshort).mean() ) df[ma_long] grouped[close_adj].transform( lambda s: s.rolling(long, min_periodslong).mean() ) df[momentum] grouped[close_adj].transform( lambda s: s.pct_change(mom) ) df[volatility] grouped[ret].transform( lambda s: s.rolling(vol, min_periodsvol).std() ) df[signal] ((df[ma_short] df[ma_long]) (df[momentum] 0)).astype(int) df.loc[df[ma_long].isna(), signal] 0 return dfmin_periods设成窗口长度避免前几行用不足窗口的数据算出虚假均线groupby(symbol)保证不同标的不会串行计算signal同时要求短均线在上和动量为正减少震荡市里的假突破。参数short、long、mom、vol不要写死在函数体里回测入口从配置读取后面做参数敏感性分析时才不用改代码。因子层只生成信号列不生成仓位仓位和费用留给回测层职责分开后排查问题会快很多。4.2 回测循环信号、仓位、手续费和滑点怎么落到收益回测最容易翻车的地方是信号时点。用 T 日收盘价算出信号却在 T 日收盘就成交等于偷看了当天收盘价。这个项目统一用shift(1)把信号推到下一交易日执行再用仓位变化计算换手和费用。下面函数把手续费和滑点合并成一个成本率简单但够用。import pandas as pd def backtest(df: pd.DataFrame, fee: float 0.0003, slippage: float 0.0005) - pd.DataFrame: df df.copy().sort_values([symbol, trade_date]) df[pos] df.groupby(symbol)[signal].shift(1).fillna(0).clip(0, 1) df[turnover] df.groupby(symbol)[pos].diff().abs().fillna(0) df[cost] df[turnover] * (fee slippage) df[strategy_ret] df[pos] * df[ret].fillna(0) - df[cost] df[nav] (1 df[strategy_ret]).groupby(df[symbol]).cumprod() df[benchmark_nav] (1 df[ret].fillna(0)).groupby(df[symbol]).cumprod() return dfshift(1)是防未来函数的关键clip(0, 1)表示只做多、不加杠杆turnover是仓位变化绝对值买入和卖出都算一次换手fee slippage合并成单边成本实际做的时候可以拆成佣金、印花税和滑点三项。strategy_ret用仓位乘标的收益再减成本nav按标的分组累乘。参数表可以这样定参数含义常用起点调整方向fee单边手续费率0.0003按账户真实费率改slippage滑点率0.0005流动性差时调高pos仓位0 或 1可扩到分批仓位shift信号延后天数1日线不要改成 04.3 结果校验基准对齐、样本外切片和交易次数检查回测跑出净值曲线只是开始校验才是重点。先看交易次数如果一年只有一两次交易均线参数可能太长如果每天换仓费用会吃掉收益。再看基准对齐用等权持有或模拟宽基作为基准确认策略收益不是靠某一段极端行情撑起来的。最后做样本外切片用前 70% 时间调参后 30% 时间只验证不回头改参数。校验代码可以很轻def validate(result: pd.DataFrame) - dict: trades int((result[turnover] 0).sum()) total_ret float(result[nav].iloc[-1] - 1) bench_ret float(result[benchmark_nav].iloc[-1] - 1) return { trades: trades, total_return: round(total_ret, 4), benchmark_return: round(bench_ret, 4), excess: round(total_ret - bench_ret, 4), }trades统计换手次数total_return和benchmark_return放在一起看excess是超额收益。校验时不要只看最后一行净值要按年度切片看避免某一年贡献全部收益。复权、停牌和信号时点三个口径对齐后净值曲线才有讨论价值否则参数越调越漂亮实盘一跑就原形毕露。5. 避坑与排查股票大数据项目最容易翻车的五个现场5.1 采集层空页不等于数据拉完了现象某只标的明明有多年日线采集脚本跑了几页就停下data/raw里的行数明显偏少。原因通常有两个page_size超过接口单页上限接口不报错但返回空数组或者请求间隔太短被限流接口返回空结果而不是 429。解决时先把page_size降到接口文档允许的范围再把sleep_seconds调到保守值并在空页时打印当前symbol、page和start/end不要直接break了事。更稳的做法是空页后做一次“重试空页”检查连续两次空页才结束同时把每次响应的条数写进日志行数之和和接口总量对不上就报警。5.2 清洗层复权因子对不齐均线全歪现象除权日附近均线突然拐头回测信号在除权前后频繁翻转。原因是没有复权或者复权因子按错误的方向乘除。前复权、后复权、不复权三种口径混用均线和动量都会失真。解决时固定一条规则因子计算只用close_adj原始close只用于展示复权因子缺失的标的不进入回测池不要用 1.0 硬填后假装没事。清洗后抽一只标的看除权日前后close_adj的日收益如果出现超过涨跌停幅度的跳变先查复权因子和停牌标记再查数据源字段是否把“每股派息”和“复权因子”混在一起。5.3 回测层信号没 shift收益好得离谱现象回测净值几乎直线向上最大回撤小得不像日线策略。原因多半是 T 日收盘信号在 T 日收盘成交等于用当天收盘价决定当天仓位未来函数直接混进来了。解决时强制signal.shift(1)并且把成交价假设写进报告是按次日开盘成交还是按次日收盘成交。用次日开盘时收益计算要改成open到open的口径不能继续用close收益。还有一个小坑是停牌日仓位在停牌日无法调整turnover要按可交易标记过滤否则费用会算多或算少。5.4 回测层手续费和滑点只写参数不写进成本现象参数表里明明有fee和slippage净值却和零成本版本一模一样。原因是成本算了但没从收益里扣或者turnover用了pos.diff()却没按标的分组第一行被当成换仓。解决时检查strategy_ret pos * ret - turnover * (fee slippage)这一行是否真的执行turnover是否groupby(symbol)后再diff。成本参数不要设成 0 做“先看看效果”零成本回测容易把高频信号误判成好策略至少用保守费率跑一遍再看成本敏感度。5.5 环境层Parquet 列类型漂移导致回测读不出来现象本地跑得好好的换一台机器或重装依赖后读取 Parquet 报类型错误或者trade_date变成字符串导致排序错乱。原因是写入时没固定 schema空值列被推断成float64日期列有的文件是datetime64有的文件是字符串。解决时在清洗层统一trade_date为datetime64[ns]symbol为字符串数值列显式astype(float64)读取时用pd.read_parquet(..., columns[...])只取需要的列并在测试里加一条 schema 断言。目录里不要混放不同批次的同名字段改字段名就新开目录别在原文件上反复覆盖。6. 二次开发把单标的脚本改成多因子研究工作台的三个改法这套代码跑通单标的后最自然的二次开发不是加更多指标而是把“脚本”改成“工作台”。第一个改法是把股票池、日期、因子参数、费用全部收进配置入口只读配置不在代码里写SIM001第二个改法是做因子注册表每个因子函数只接收 DataFrame、返回一列信号回测层按名字组合后面加新因子不用改回测循环第三个改法是报告自动化每次回测固定输出净值图、年度收益、交易次数和样本外切片别让结果只留在 notebook 里。改造点改什么验证方法配置化股票池settings.yaml管标的和日期换一组标的不用改代码因子注册表因子函数统一入参出参新因子能单独跑通再组合报告自动化固定输出净值、回撤、交易次数同一配置重复跑结果一致我一般还会加一条最小测试同一份清洗数据跑两次回测净值序列必须完全一致把信号shift(1)改成shift(0)收益应该明显变好这说明测试能抓到未来函数。改造时不要一次动四层先把采集和清洗固定住再动因子和回测每改一层先用三只模拟标的跑一遍基准对齐。从那以后我每次改策略参数都强制先跑基准对齐和样本外切片再决定要不要调参。希望帮到你。本文还有配套的精品资源点击获取