news 2026/11/6 9:03:28

Python股票大数据分析:爬虫、复权清洗与量化回测实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python股票大数据分析:爬虫、复权清洗与量化回测实战

简介:面向希望将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 tests

python -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/report"

symbols是股票池入口,建议单独维护一个文本文件,配置文件只引用路径;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跑完先看交易次数,再看净值,交易次数为零或高得离谱都说明信号或时点有问题。手续费和滑点先用保守值试跑,比如fee=0.0003、slippage=0.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(encoding="utf-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": f"Bearer {os.getenv('DEMO_API_TOKEN', '')}"} for attempt in range(3): try: resp = requests.get(url, params=params, headers=headers, timeout=10) 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, page=page) items = payload.get("items", []) if not items: break rows.extend(items) state.setdefault(symbol, {})["next_page"] = page + 1 STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8") page += 1 time.sleep(0.8) # 限频,按接口额度调整 if not rows: return 0 df = pd.DataFrame(rows) RAW_DIR.mkdir(parents=True, exist_ok=True) df.to_parquet(RAW_DIR / f"{symbol}.parquet", index=False) return len(df)

fetch_page负责单页请求,timeout=10防止连接挂死,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"], keep="last") 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 df

drop_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_keys=False) df["ma_short"] = grouped["close_adj"].transform( lambda s: s.rolling(short, min_periods=short).mean() ) df["ma_long"] = grouped["close_adj"].transform( lambda s: s.rolling(long, min_periods=long).mean() ) df["momentum"] = grouped["close_adj"].transform( lambda s: s.pct_change(mom) ) df["volatility"] = grouped["ret"].transform( lambda s: s.rolling(vol, min_periods=vol).std() ) df["signal"] = ((df["ma_short"] > df["ma_long"]) & (df["momentum"] > 0)).astype(int) df.loc[df["ma_long"].isna(), "signal"] = 0 return df

min_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 df

shift(1)是防未来函数的关键;clip(0, 1)表示只做多、不加杠杆;turnover是仓位变化绝对值,买入和卖出都算一次换手;fee + slippage合并成单边成本,实际做的时候可以拆成佣金、印花税和滑点三项。strategy_ret用仓位乘标的收益再减成本,nav按标的分组累乘。参数表可以这样定:

参数含义常用起点调整方向
fee单边手续费率0.0003按账户真实费率改
slippage滑点率0.0005流动性差时调高
pos仓位0 或 1可扩到分批仓位
shift信号延后天数1日线不要改成 0

4.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),收益应该明显变好,这说明测试能抓到未来函数。改造时不要一次动四层,先把采集和清洗固定住,再动因子和回测;每改一层,先用三只模拟标的跑一遍基准对齐。从那以后我每次改策略参数,都强制先跑基准对齐和样本外切片,再决定要不要调参。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 8:26:49

Folia 全屏歌词播放器怎么安装?新手一次装对,从下载到首次播放

Folia 全屏歌词播放器怎么安装&#xff1f;新手一次装对&#xff0c;从下载到首次播放 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 A multi-platform music player for local files, Navidrome, and online stream…

作者头像 李华
网站建设 2026/11/6 8:23:45

EtherCAT实时总线核心原理与从站开发调试全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 8:15:13

网页论文公式复制到Word:MathML与LaTeX无损转换指南

1. 先看清网页上那些公式到底是什么东西网页上的论文公式&#xff0c;看着都是一样的黑色符号&#xff0c;落到剪贴板里却是完全不同的东西。我刚接手论文排版那阵子&#xff0c;最常干的事就是把网页上的公式选中、CtrlC、切到 Word、CtrlV&#xff0c;然后盯着屏幕上的一堆问…

作者头像 李华
网站建设 2026/11/6 8:14:44

MinIO监控体系:Prometheus+Grafana实战指南

1. 监控体系构建的必要性在分布式存储领域&#xff0c;MinIO作为高性能的对象存储解决方案&#xff0c;其运行状态监控一直是运维工作的重点难点。传统的人工巡检方式存在响应滞后、指标片面等问题&#xff0c;我们迫切需要建立一套自动化监控体系。PrometheusGrafana的组合恰好…

作者头像 李华
网站建设 2026/11/6 8:12:33

软件建模中质量需求与组织愿景的区分与实践

1. 从质量需求到组织愿景&#xff1a;软件建模中的常见误区解析在软件建模和需求分析过程中&#xff0c;我见过太多团队陷入一个典型误区——将系统质量需求直接等同于组织愿景。这种现象在敏捷开发普及的今天尤为常见&#xff0c;许多产品经理和技术人员习惯于从系统功能和技术…

作者头像 李华