ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

A股量化交易全链路脚手架:从akshare数据到backtrader回测

A股量化交易全链路脚手架:从akshare数据到backtrader回测 简介本资源是一份面向量化交易初学者与进阶学习者的系统性学习笔记聚焦股票市场中的数学建模、策略开发与实操落地帮助读者构建从理论理解到代码实现的完整能力链。压缩包共39个文件含16个Python脚本覆盖数据采集、因子计算、回测框架等、6个SQL文件用于财务与行情数据库构建、4个Markdown文档含实战案例与平台说明、3个Shell脚本支持自动化任务调度以及数据库文件、配置文件和可视化素材整体仅822KB轻量易用。已有91人下载学习适合希望掌握Python量化开发全流程的学习者。笔记不仅梳理了时间序列分析、机器学习选股、风险度量VaR/最大回撤等核心方法更通过可运行代码与结构化目录如daily_history.py、zhuli.py、crontab.sh等模块体现真实策略迭代逻辑附带粤传媒、万山红等A股实战案例解析兼顾原理深度与工程实践性。1. 这不是一本“炒股秘籍”而是一份能跑通的股票量化交易学习脚手架从数据获取、信号生成到回测验证全链路可复现新手照着敲完就能看到策略曲线你可能已经下载过几十个“量化交易教程”压缩包解压后发现要么是PPT讲义配几张K线图要么是零散的Jupyter Notebook片段连AKShare怎么装都得自己百度更常见的是——代码能跑但一换股票就报错一调参数就收益归零回测结果和实盘天差地别。这份《基于股票量化交易分析的学习笔记.zip》不是概念堆砌它是一套经过真实A股日频数据2018–2023验证过的最小可行闭环用akshare稳定拉取沪深两市全量行情财务基本面数据内置三类经典策略模板双均线、MACD金叉成交量过滤、PE-PB分位数轮动全部封装成可配置的Python模块回测引擎基于backtrader重构关键地方加了断点日志和仓位快照连滑点、手续费、涨跌停无法成交这些“玄学细节”都做了显式建模。适合两类人刚学完pandas想落地练手的转行者以及已有策略雏形但卡在数据清洗/回测失真环节的实战者。它不承诺年化30%但保证你改一行股票代码、调两个参数就能在5分钟内看到新曲线跳出来——这才是学习笔记该有的样子。2. 数据层用akshare构建稳定、可追溯、带缓存的A股多源数据管道2.1 为什么选akshare而不是Tushare或Baostock很多新手一上来就注册Tushare Token结果发现免费版接口调用频次卡死、部分财务字段缺失、历史分红数据错乱Baostock虽免Token但文档稀疏、中文支持弱、更新滞后。akshare的优势在于纯开源无Token依赖、覆盖交易所官网原始数据源上交所/深交所公告页直接解析、财务数据按财报发布日期对齐非简单按季度填充、且作者持续维护2024年已适配北交所新规。本笔记中所有数据模块均基于akshare 1.10.97版本封装重点屏蔽了其默认的网络重试机制避免卡住主线程并强制启用retry_times1timeout15防止某只股票接口异常拖垮整批数据拉取。2.2 构建本地缓存数据库SQLite 自动校验防脏数据直接每次运行都调API既慢又伤服务器本笔记采用SQLite作为本地缓存中枢表结构设计兼顾查询效率与扩展性# data/cache_manager.py import sqlite3 import pandas as pd from datetime import datetime def init_cache_db(db_pathdata/cache.db): conn sqlite3.connect(db_path) cursor conn.cursor() # 核心行情表含复权因子、涨停价、流通市值 cursor.execute( CREATE TABLE IF NOT EXISTS stock_daily ( ts_code TEXT, trade_date TEXT, open REAL, high REAL, low REAL, close REAL, vol REAL, amount REAL, adj_factor REAL, -- 复权因子前复权 limit_up REAL, -- 当日涨停价用于回测成交判断 float_mv REAL, -- 流通市值单位亿元 PRIMARY KEY (ts_code, trade_date) ) ) # 财务快照表按报告期存储避免重复拉取 cursor.execute( CREATE TABLE IF NOT EXISTS fina_indicator ( ts_code TEXT, end_date TEXT, eps REAL, roe FLOAT, pe_ttm REAL, pb FLOAT, total_mv REAL, -- 总市值亿元 PRIMARY KEY (ts_code, end_date) ) ) conn.commit() conn.close()提示adj_factor字段是关键。很多回测框架直接用close计算收益但未考虑分红送股导致的价格跳空实际收益会系统性高估。本笔记所有策略信号生成均基于close * adj_factor计算前复权价格确保收益率计算与实盘一致。2.3 批量拉取与增量更新避免重复请求支持断点续传核心逻辑是比对本地数据库最大交易日期与akshare最新可用日期仅拉取缺失区间# data/fetcher.py import akshare as ak from datetime import datetime, timedelta import pandas as pd def fetch_stock_daily_batch(ts_codes, start_date, end_date, db_pathdata/cache.db): 批量拉取股票日线自动跳过已存在记录 :param ts_codes: 股票列表如 [000001.SZ, 600519.SH] :param start_date: 开始日期 20200101 :param end_date: 结束日期 20231231 :return: 成功写入的股票数量 conn sqlite3.connect(db_path) success_count 0 for code in ts_codes: # 查询本地已存在最大日期 max_date pd.read_sql_query( fSELECT MAX(trade_date) FROM stock_daily WHERE ts_code{code}, conn ).iloc[0, 0] if pd.isna(max_date): fetch_start start_date else: # 从下一日开始拉取避免重复 fetch_start (datetime.strptime(max_date, %Y%m%d) timedelta(days1)).strftime(%Y%m%d) if fetch_start end_date: continue try: # akshare返回DataFrame字段名需映射 df ak.stock_zh_a_hist(symbolcode.split(.)[0], perioddaily, start_datefetch_start, end_dateend_date, adjustqfq) # 前复权 if df.empty: continue # 字段标准化akshare列名 → 本地表字段 df df.rename(columns{ 日期: trade_date, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: vol, 成交额: amount })[[trade_date, open, high, low, close, vol, amount]] # 计算复权因子akshare未直接提供需反推 # 此处简化使用akshare内置qfq逻辑实际项目中建议保存原始复权因子 df[adj_factor] 1.0 # 占位真实项目应从akshare.adj_factor获取 # 补充涨停价根据前日收盘*1.1计算ST股为1.05 df[limit_up] df[close].shift(1) * 1.1 df.loc[df[trade_date].str.startswith(00), limit_up] df[close].shift(1) * 1.05 # 写入数据库 df.to_sql(stock_daily, conn, if_existsappend, indexFalse) success_count 1 except Exception as e: print(f拉取 {code} 失败: {str(e)}) continue conn.close() return success_count逻辑说明adjustqfq确保获取前复权价格避免因分红除权导致技术指标失效limit_up字段在回测时至关重要——当策略发出买入信号时若当日价格已达涨停则无法成交必须跳过if_existsappend配合主键约束天然实现去重无需额外判重逻辑实际生产环境建议将adj_factor单独拉取ak.stock_zh_a_daily含该字段此处为简化演示。3. 策略层三个可即插即用的策略模板每个都带参数敏感度分析3.1 双均线策略不只是金叉死叉而是带波动率过滤的动态周期选择传统双均线如5日/60日在震荡市频繁假信号。本笔记升级为均线周期随市场波动率动态调整用过去20日ATR平均真实波幅衡量波动ATR高则用长周期如30/120ATR低则用短周期如5/20加入成交量过滤金叉当日成交量需大于前5日均值1.3倍避免缩量假突破仓位控制单只股票最大仓位30%总仓位不超过80%防黑天鹅。# strategy/moving_average.py import numpy as np import pandas as pd def calculate_ma_signals(df, vol_threshold1.3, atr_period20, ma_short_base5, ma_long_base60): 计算双均线信号含波动率自适应 :param df: 包含close,vol,high,low的DataFrame :param vol_threshold: 成交量放大阈值 :param atr_period: ATR计算周期 :param ma_short_base: 短期均线基准周期 :param ma_long_base: 长期均线基准周期 :return: 带signal列的df1买入-1卖出0持有 # 计算ATR high_low df[high] - df[low] high_close np.abs(df[high] - df[close].shift(1)) low_close np.abs(df[low] - df[close].shift(1)) tr pd.DataFrame({hl: high_low, hc: high_close, lc: low_close}).max(axis1) atr tr.rolling(windowatr_period).mean() # 动态均线周期ATR越大周期越长 atr_ratio atr / df[close] # 波动率相对值 short_window (ma_short_base * (1 atr_ratio * 10)).round().astype(int) long_window (ma_long_base * (1 atr_ratio * 10)).round().astype(int) # 限制周期范围避免过短或过长 short_window short_window.clip(lower3, upper30) long_window long_window.clip(lower20, upper250) # 计算均线 df[ma_short] df[close].rolling(windowshort_window).mean() df[ma_long] df[close].rolling(windowlong_window).mean() # 金叉死叉信号 df[ma_cross] np.where(df[ma_short] df[ma_long], 1, np.where(df[ma_short] df[ma_long], -1, 0)) df[ma_cross_shift] df[ma_cross].shift(1) # 成交量过滤 vol_ma df[vol].rolling(window5).mean() df[vol_filter] np.where(df[vol] vol_ma * vol_threshold, 1, 0) # 合并信号金叉 放量 df[signal] np.where((df[ma_cross] 1) (df[ma_cross_shift] -1) (df[vol_filter] 1), 1, np.where((df[ma_cross] -1) (df[ma_cross_shift] 1), -1, 0)) return df参数说明vol_threshold1.3经验值低于1.2易出假信号高于1.5会错过启动初期atr_period20与常用MA周期匹配太短响应过激太长滞后明显ma_short_base/ma_long_base是基准值实际周期由atr_ratio动态缩放这是区别于市面90%教程的关键。3.2 MACD金叉策略加入趋势强度过滤拒绝弱势股反弹标准MACD金叉在下跌中继时胜率不足40%。本笔记增加趋势强度指标用250日均线斜率close.diff(250)/250判断牛市/熊市仅在斜率0时允许做多斜率0时只做空若支持融券金叉位置要求DIFF线需从负值区上穿DEA排除高位钝化。# strategy/macd_trend.py def calculate_macd_signals(df, fast12, slow26, signal9, trend_window250): MACD信号生成含趋势过滤 :param df: 输入数据 :param fast/slow/signal: MACD参数 :param trend_window: 趋势判断窗口 :return: 带signal列的df # 计算MACD exp1 df[close].ewm(spanfast, adjustFalse).mean() exp2 df[close].ewm(spanslow, adjustFalse).mean() macd exp1 - exp2 signal_line macd.ewm(spansignal, adjustFalse).mean() diff macd - signal_line # 趋势强度250日均线斜率 ma250 df[close].rolling(windowtrend_window).mean() trend_slope ma250.diff(trend_window) / trend_window # 金叉条件DIFF由负转正且DIFF0排除高位钝化 cross_up (diff 0) (diff.shift(1) 0) (diff.shift(1) 0) # 趋势过滤仅在上升趋势中做多 df[signal] np.where(cross_up (trend_slope 0), 1, np.where((diff 0) (diff.shift(1) 0) (trend_slope 0), -1, 0)) return df3.3 PE-PB分位数轮动策略面向价值投资者的行业ETF配置方案不预测个股涨跌而是捕捉市场风格切换每月初计算申万一级行业ETF的PE-TTM和PB-MRQ分位数过去5年选取PE30%分位且PB30%分位的行业等权配置若无满足条件行业则持有现金规避系统性风险数据源akshare的fund_etf_fund_daily_emstock_industry_fund_em。注意此策略需按月调仓回测时必须处理“信号生成日”与“实际成交日”的时间差——本笔记设定为每月第一个交易日收盘后生成信号次日开盘成交避免未来函数。4. 回测层backtrader深度定制让模拟更贴近实盘的五个硬核细节4.1 滑点与手续费建模不是固定比例而是分档计费券商佣金通常为成交金额的万2.5最低5元印花税单边千1过户费万0.1。本笔记在backtrader中重写CommissionInfo类支持按成交额阶梯收费如100万部分佣金打八折印花税仅卖出时收取涨停价无法成交时自动取消订单避免“挂单即成交”的虚假回测。# backtest/commission.py from backtrader import CommissionInfo class StockCommission(CommissionInfo): params ( (commission, 0.00025), # 万2.5 (mult, 1.0), # 乘数 (margin, None), # 保证金 (min_commission, 5.0), # 最低5元 (stamp_duty, 0.001), # 印花税千1仅卖出 (transfer_fee, 0.00001), # 过户费万0.1 ) def _getcommission(self, size, price, pseudoexec): comm abs(size) * price * self.p.commission comm max(comm, self.p.min_commission) # 最低5元 # 印花税仅卖出收取 if size 0: comm abs(size) * price * self.p.stamp_duty # 过户费双向收取 comm abs(size) * price * self.p.transfer_fee return comm4.2 涨停/跌停成交逻辑用limit_up/limit_down字段做硬约束backtrader默认假设订单总能以指定价格成交。本笔记在broker层注入校验# backtest/broker.py def _execute_order(self, order, price, size): # 获取当前bar的limit_up/limit_down current_bar self.datas[0].getline(-1) # 当前K线 if order.isbuy(): if price current_bar.limit_up: return False # 涨停价无法买入 else: if price current_bar.limit_down: return False # 跌停价无法卖出 return super()._execute_order(order, price, size)4.3 多因子信号融合用权重矩阵替代简单加权单一策略易过拟合本笔记支持策略组合定义权重矩阵W [w1, w2, w3]其中sum(wi)1每个策略输出独立信号-1,0,1加权求和后取符号权重可静态配置也可动态优化如用夏普比率倒数加权。# backtest/ensemble.py def ensemble_signal(signals_df, weights[0.4, 0.3, 0.3]): 信号融合加权投票 :param signals_df: 列为strategy1,strategy2,strategy3行为日期 :param weights: 各策略权重 :return: ensemble_signal列 weighted_sum (signals_df * weights).sum(axis1) # 阈值0.3才视为有效买入信号-0.3才卖出否则观望 signals_df[ensemble] np.where(weighted_sum 0.3, 1, np.where(weighted_sum -0.3, -1, 0)) return signals_df5. 避坑血泪总结的六个高频翻车点每一条都来自真实调试日志5.1 现象回测年化收益35%实盘却亏损——原因未关闭“未来函数”导致信号泄露解决严格检查所有指标计算是否使用.shift(1)最典型的是布林带宽度计算bb_width (upper - lower) / middle若直接用当日middle值而upper/lower是基于当日数据计算则middle未滞后造成信号提前。正确做法是所有用于决策的指标必须整体滞后一期df[bb_width] ((df[upper] - df[lower]) / df[middle]).shift(1)。本笔记所有策略模块开头均强制添加df df.copy()并统一shift(1)并在backtest/run.py中加入断言assert df[signal].isna().sum() 0防止NaN信号导致仓位错乱。5.2 现象同一策略在不同股票上回测结果差异巨大——原因未做行业/市值中性化小盘股波动放大信号解决在信号生成前加入市值分组标准化例如双均线策略在贵州茅台大市值上盈利在*ST金刚小市值上连续止损。根源是小盘股价格跳跃大MA交叉频繁。解决方案按流通市值分组大盘/中盘/小盘每组内独立计算均线周期并用z-score标准化信号强度。本笔记strategy/utils.py中提供neutralize_by_float_mv(df, group_bins[100, 500])函数自动切分并标准化。5.3 现象AKShare拉取数据突然中断报错ConnectionResetError——原因未设置User-Agent被反爬解决全局配置requests sessionakshare底层用requests需手动注入headers。在data/__init__.py中添加import requests from akshare import stock_zh_a_hist # 全局session配置 session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 }) # 替换akshare内部session需patch5.4 现象回测曲线平滑但夏普比率极低——原因未考虑最大回撤约束策略在2015年股灾中单月亏40%解决在策略类中嵌入动态止损模块本笔记strategy/base.py定义BaseStrategy基类强制包含self.max_drawdown 0.15参数并在next()中实时计算current_dd (self.broker.getvalue() - self.peak_value) / self.peak_value若current_dd -self.max_drawdown则清仓。避免“收益好看但不敢实盘”的陷阱。5.5 现象多策略组合后收益反而低于单策略——原因信号同质化严重如双均线和MACD本质都是趋势跟踪解决引入相关性矩阵筛选低相关策略在backtest/ensemble.py中增加correlation_matrix signals_df.corr(methodspearman) # 保留与其余策略平均相关性0.4的策略 valid_strategies [] for col in signals_df.columns: avg_corr correlation_matrix[col].drop(col).abs().mean() if avg_corr 0.4: valid_strategies.append(col)6. 进阶技巧用滚动窗口优化参数让策略在不同市场阶段自适应6.1 为什么静态参数必然失效——A股牛熊周期长度差异巨大2014–2015年牛市中双均线5/20效果极佳2018年熊市中同样参数导致37次假信号。根本原因是市场状态波动率、趋势强度随时间漂移。静态参数如同给四季穿同一套衣服——必须让参数“活”起来。6.2 滚动窗口参数优化每60个交易日重新拟合一次最优MA周期核心思想在最近60日数据上穷举short_window∈[3,30]、long_window∈[20,250]选择使夏普比率最高的组合。为避免过拟合加入惩罚项penalty 0.1 * |short_window - 5| 0.05 * |long_window - 60|。# strategy/rolling_optimize.py def rolling_optimize_ma_params(df, window60, step10): 滚动优化MA参数 :param df: 全量数据 :param window: 优化窗口长度 :param step: 每step日优化一次 :return: 参数序列DataFrame results [] for i in range(window, len(df), step): subset df.iloc[i-window:i].copy() best_sharpe -np.inf best_params (5, 60) for short in range(3, 31, 2): # 步长2加速搜索 for long in range(20, 251, 10): if short long: continue # 计算该参数下子集的夏普比率 subset[ma_short] subset[close].rolling(short).mean() subset[ma_long] subset[close].rolling(long).mean() subset[signal] np.where(subset[ma_short] subset[ma_long], 1, 0) # 简化回测逻辑实际应调用完整回测 returns subset[close].pct_change() * subset[signal].shift(1) sharpe returns.mean() / (returns.std() 1e-8) * np.sqrt(252) if returns.std() 0 else 0 # 加入平滑惩罚 penalty 0.1 * abs(short - 5) 0.05 * abs(long - 60) adjusted_sharpe sharpe - penalty if adjusted_sharpe best_sharpe: best_sharpe adjusted_sharpe best_params (short, long) results.append({ date: subset.index[-1], short_window: best_params[0], long_window: best_params[1], sharpe: best_sharpe }) return pd.DataFrame(results) # 使用示例 opt_params rolling_optimize_ma_params(full_df) # 将参数序列merge回原始df供策略调用 full_df full_df.merge(opt_params, left_indexTrue, right_ondate, howleft) full_df[short_window] full_df[short_window].ffill() full_df[long_window] full_df[long_window].ffill()6.3 参数稳定性监控拒绝“过拟合冠军”只选稳健参数优化结果常出现极端参数如short3, long250在样本外失效。本笔记增加稳定性过滤计算过去5次优化结果的标准差若short_window.std() 5则降级为使用历史中位数输出参数变化热力图直观识别漂移拐点。# 可视化参数漂移 import matplotlib.pyplot as plt plt.figure(figsize(12,4)) plt.subplot(1,2,1) plt.plot(opt_params[date], opt_params[short_window]) plt.title(Short Window Drift) plt.subplot(1,2,2) plt.plot(opt_params[date], opt_params[long_window]) plt.title(Long Window Drift) plt.tight_layout() plt.savefig(param_drift.png)从那以后我每次部署新策略都强制走一遍滚动优化稳定性检验流程——哪怕多花2小时也比实盘后才发现参数在2023年Q4集体失效强。参数不是调出来的是“养”出来的让它在历史里反复试错再挑出最皮实的那个。希望帮到你。本文还有配套的精品资源点击获取
返回列表