ARTICLE DETAIL

资讯详情

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

用Tushare构建Python量化交易系统:从数据到回测完整实践

用Tushare构建Python量化交易系统:从数据到回测完整实践 先交代一下背景大概半年前我还是一个每天打开行情软件盯分时图、靠直觉买卖的散户。后来被一个做私募的朋友刺激到了人家做交易靠的不是盘感是系统。于是我也决定搞一套自己的“系统”但真动手才发现做量化最难的不是写策略而是搞数据。国内A股的数据接口又多又杂高质量的往往要花钱绕了一圈之后我锁定了Tushare这套开源数据接口靠它把第一个量化交易系统完整地搭了出来。这篇文章不打算讲什么高深的大模型选股就把我踩过的坑、调通的代码、验证过的流程一步步写下来。整体比较适合有Python基础、对量化交易完全陌生、想从0到1跑通一套系统的朋友。你会发现量化交易的核心其实就三件事数据、策略和执行而数据是绝对的地基。1. 项目背景从解决自己的痛点开始1.1 我为什么要做量化交易系统市面上主流的行情软件都能看盘但它们的共同问题是数据拿不出来至少不好批量拿出来。我想统计“过去三年哪些股票在均线金叉后表现更好”用Excel手动翻根本不现实。人工复盘和程序化回测的效率差距就像用手工记账和用ERP系统记账的差别一样巨大。我当时给自己定的目标很明确先不追求赚钱追求流程闭环。也就是说我要能在程序里完成这样一件事——自动拉取历史行情、按规则产生买卖信号、模拟资金变化、算出这策略到底行不行。这个流程跑通了后面换什么策略都是顺手的事。1.2 数据源为什么选了Tushare选数据源那几天我几乎把网上能搜到的免费数据源都试了一遍。有的数据源更新不及时有的接口文档写得像天书还有的爬虫方案每次打开网页结构一变就要重写。Tushare在当时是综合体验最好的数据覆盖A股股票、指数、基金、期货这些我关心的品种并且提供Python SDK几行代码就能拿到结构化数据。Tushare的Pro版需要token令牌来控制权限这个机制有点像去图书馆借书不同等级的借阅证能借的书不一样。普通注册用户就能获取大部分基础数据积分往上走能解锁更高频率和更多字段。对个人做研究来说免费额度基本够用。1.3 系统的整体架构长什么样我搭建的系统分成四层每一层解决一个问题数据层负责从Tushare拉取行情、交易日历存到本地数据库做好清洗和增量更新。策略层把交易规则写成代码比如“5日均线上穿20日均线就买入”输入历史数据输出具体的买卖信号。回测层把信号放到历史K线上模拟撮合统计收益、回撤、胜率等指标。执行层通过定时任务获取最新数据判断当前是否满足信号条件在模拟盘里执行下单操作。这个架构算是量化系统里很标准的骨架后面不管你是做因子模型还是机器学习选股都跳不出这个框架。我的理念是先搭骨架再填肉尽量避免一开始就陷入细节泥潭。2. 数据层先把Tushare数据接到碗里来2.1 注册、Token与权限机制用Tushare的第一步是去官网注册账号然后在个人主页找到token令牌这个token相当于你调用数据接口的钥匙。调用接口时会带token服务器识别你的身份和权限。我在这一步犯过的最大错误是把token直接硬编码写在脚本里后来代码传到公开仓库token相当于裸奔。建议把token放在环境变量或者本地配置文件中。关于积分机制特别想强调一点别一上来就疯狂充值积分。基础积分可以满足日线行情、股票列表、交易日历的大部分需求先做研究和回测完全够。很多高积分接口是分钟级高频数据或者更细粒度的财务数据等你的系统真的跑起来了再考虑不迟。2.2 第一段代码拉取交易日历和股票列表安装Tushare很简单直接pip安装即可。我第一次成功调用接口时先拉了一份交易日历代码大致是这样的import tushare as ts import os # 推荐方式从环境变量读取token而不是写死在代码里 token os.environ.get(TUSHARE_TOKEN, 你的token) ts.set_token(token) pro ts.pro_api() # 获取上海证券交易所2020年至今的交易日历 calendar pro.trade_cal(exchangeSSE, start_date20200101, end_date20250101) print(calendar.head())交易日历是整个系统的时间基准因为A股有休市、调休直接用自然日去推断交易日会出大问题。比如周一虽然是工作日但遇到节假日就休市如果你按自然日去拉行情就会拉到空数据。交易日历就是用来规避这个问题的。紧接着拉一份当前在市的所有股票列表# 获取股票基础信息list_statusL代表上市状态 stock_list pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,industry,market,list_date) print(stock_list.shape) print(stock_list.head())stock_basic接口返回的字段里有ts_code这是Tushare统一的股票代码格式比如平安银行的代码是000001.SZ。后面的所有行情接口都依赖这个ts_code去关联数据所以第一步一定要把股票列表存好它相当于整个系统的“花名册”。2.3 日线行情与增量更新获取股票日线行情的接口是pro.daily我最初的做法是循环所有股票代码逐个拉取历史数据。这里有一个性能大坑如果几千只股票都全量拉一遍接口会被限流而且耗时极长。更合理的方案是做增量更新。所谓增量更新就是只在本地数据库里存一份全量历史数据之后每天只需要拉最新一天的行情。思路是先查本地数据里每只股票的最大日期然后只拉“最新日期1”到“今天”的数据。# 以平安银行为例拉取2024年以来的日线数据 df pro.daily(ts_code000001.SZ, start_date20240101, end_date20250201) print(df.head())daily接口返回的字段包括ts_code、trade_date、open开盘价、high最高价、low最低价、close收盘价、pre_close昨收价、change涨跌额、pct_chg涨跌幅、vol成交量手、amount成交额千元。其中vol的单位是手1手等于100股这个细节一开始没注意会导致仓位计算差100倍。2.4 数据清洗、复权与本地备份有了原始数据清洗这一步躲不开。我总结出三个必做动作去重同一只股票同一天可能出现多条记录比如重复拉取时按ts_code和trade_date去重。排序按trade_date升序排列后续算均线、收益率都依赖时间顺序。缺失值处理停牌股在停牌区间没有日线数据回测时要么前向填充要么直接跳过。最关键的还是复权问题。股票会分红、送股导致价格出现跳空缺口所以原始价格不能直接用于计算收益率。Tushare提供复权因子接口pro.adj_factor使用时把收盘价乘上复权因子得到后复权价格再做策略信号计算。回测必须用后复权数据这样避免了分红除权造成的假跌假涨。数据备份也是我吃了亏才补上的。有一回电脑系统崩溃重装之后本地SQLite数据库没备份几百MB的行情数据全丢了重新拉又花了一整天。后来我设置了一个每周自动备份的任务直接复制数据库文件到移动硬盘和云盘。数据是量化的资产备份就是给资产上保险。3. 策略与回测让数据产生交易信号3.1 为什么第一策略选双均线很多初学者一上来就想做涨停板打板、龙虎榜跟随这些策略不适合起步阶段。我的第一个策略选的是最常见的双均线系统当5日均线上穿20日均线时买入当5日均线下穿20日均线时卖出。选它的原因也很简单逻辑透明、代码短、可解释性强。均线本质上是平滑价格波动金叉死叉是趋势反转的标准信号。如果一个简单到极致的策略能跑出说得过去的结果说明整个框架是对的如果框架有bug在简单策略上也更容易暴露。先把策略需要的数据准备好import pandas as pd import numpy as np # 假设df是从Tushare拉到的某股票日线数据已按日期升序排列 # 这里用后复权收盘价做计算更准确 close df[close] # 计算5日和20日移动平均线 df[ma5] close.rolling(window5).mean() df[ma20] close.rolling(window20).mean() # 生成信号列默认0表示无操作1表示买入-1表示卖出 df[signal] 0 df.loc[df[ma5] df[ma20], signal] 1 df.loc[df[ma5] df[ma20], signal] -1这一步的输出就是策略层的结果每一天都有一个信号值1代表多头趋势-1代表空头趋势。后面的回测层只需要消费这组信号就行。3.2 计算信号时最容易被坑的未来函数未来函数是回测里最隐蔽的杀手通俗说就是“用到了当时根本不可能知道的数据”。在计算均线信号时也有这个坑如果用第T天的收盘价计算均线并且默认第T天就以这个信号成交相当于你在收盘那一刻才算出信号、却用当天的价格成交了这在实际交易中几乎做不到。正确的做法是把信号向后平移一天也就是今天收盘后产生信号明天开盘再执行交易。代码上就是shift操作# 把signal列整体向后移动一天避免用到当天收盘后的未来信息 df[position] df[signal].shift(1).fillna(0) # 只有position发生变化的日期才交易这就是我们要的买卖点 df[trade] df[position].diff()这个细节直接决定了回测结果可信不可信。出现“某策略年化收益率300%”这种离谱结果时第一件事就是检查有没有未来函数。3.3 原子级的回测引擎怎么写回测引擎听起来高大上拆开了其实就是模拟一个资金账户逐日推进把持仓市值和现金余额加起来算总资产。我一开始想用现成的回测框架但发现自己手写一个几十行的引擎反而更容易理解交易撮合的每一个细节。下面是最简版本的核心逻辑def run_backtest(df, initial_cash1000000): cash initial_cash # 现金 position 0 # 持仓股数 portfolio_value [] # 每日总资产 for i in range(len(df)): date df.index[i] price df.loc[date, close] # 假设收盘价成交 signal df.loc[date, position] # 买入信号全仓买入 if signal 1 and position 0: position int(cash / (price * 100)) * 100 # 按手数取整 cash - position * price # 卖出信号清仓 elif signal -1 and position 0: cash position * price position 0 portfolio_value.append(cash position * price) df[portfolio_value] portfolio_value return df这个引擎忽略了很多现实因素比如手续费、滑点、涨跌停不能成交但它是理解回测本质的一把钥匙。先掌握这个核心再慢慢往里面加各种约束条件比一上来就用复杂框架好理解得多。3.4 怎么看绩效收益、回撤与夏普跑完回测不能只看最后赚了多少钱有些数字必须算累计收益率期末总资产除以初始资金减1。年化收益率把累计收益率换算成一年的水平。最大回撤从最高点到最低点的最大跌幅衡量你最多会“亏”多少。夏普比率每承担一单位风险能换来多少超额收益。df[return] df[portfolio_value].pct_change() total_return df[portfolio_value].iloc[-1] / initial_cash - 1 # 最大回撤计算 cum_max df[portfolio_value].cummax() drawdown df[portfolio_value] / cum_max - 1 max_drawdown drawdown.min() # 夏普比率年化后的超额收益除以波动 daily_rf 0.02 / 252 # 年化无风险利率约2%除以252个交易日 excess_ret df[return] - daily_rf sharpe np.sqrt(252) * excess_ret.mean() / excess_ret.std() print(f累计收益率: {total_return:.2%}) print(f最大回撤: {max_drawdown:.2%}) print(f夏普比率: {sharpe:.2f})我跑完双均线策略之后最直观的感受是A股市场上靠简单均线赚钱没那么容易回撤常年超过20%年化收益也就比存定期好一点还要承担波动。但这个结果其实非常有价值——它告诉我一个免费的普通策略大概是什么水平后面做优化时有了参照系。4. 模拟盘把纸上富贵当成真实交易来跑4.1 模拟盘的定位专门用来测试交易执行历史回测跑得再好不代表实盘就能赚钱因为回测是基于“过去已成事实”的模拟。模拟盘也叫纸上交易或盘感训练的意义在于按真实市场的行情去执行策略但不投真钱。我的模拟盘结构很简单每天早上定时拉最新行情算信号做出买卖决策。刚开始我用的是“看一眼信号再手工在券商模拟盘下单”后来发现手工操作太慢、太容易漏单于是直接用程序自动下单到一家券商的模拟交易接口。4.2 用定时任务做行情轮询与自动下单执行层的核心代码实际上是一个轮询任务import time import datetime as dt def on_bar(): # 1. 获取当前所有持仓股票的最新日线数据 # 2. 重新计算均线和信号 # 3. 如果出现买入信号发送买入订单 # 4. 如果出现卖出信号发送卖出订单 pass def run(): while True: now dt.datetime.now() # 交易时间9:30-11:3013:00-15:00 if is_trading_time(now): on_bar() time.sleep(60) # 每分钟检查一次 if __name__ __main__: run()我把它挂在一台轻量服务器上保证每天都能自动运行。但要说实话服务器方案也有弊端比如网络波动导致接口调用失败、断网时无法恢复任务等。后来我又加了重试机制和告警通知才算基本稳定。如果你只是个人研究跑在本地电脑上、每天盯一眼日志也够用。4.3 交易成本佣金、印花税和滑点模拟盘和回测最大的不同在于交易成本。A股的费用大致是这样的费用项费率说明佣金万1.5到万3不等买卖双向收取有最低5元限制印花税0.05%2023年8月后仅在卖出时收取过户费0.001%买卖双向收取绝大多数券商自动算除此之外还有滑点也就是你想买入时实际成交价格比报价高一点、想卖出时比报价低一点。回测时我一般会按单边万5去模拟滑点。这些成本看起来不起眼但高频交易或者频繁调仓的情况下一年下来能吞掉很大一部分收益。我的双均线策略调仓频率不算高一年大概几十次交易成本大约能吃掉2%到3%的年化收益这个数字一定要心里有数。4.4 持仓与风控不能把鸡蛋放在一个篮子里实盘模拟和回测还有一个显著区别回测时可以假设全仓一只股票但真实交易中这样玩无异于赌博。我给自己的模拟盘定了几条风控铁律单只股票持仓不超过总资金的20%。总仓位最多8成永远保留2成现金应对突发情况。单笔亏损超过8%强制止损不找理由死扛。最多同时持有5只股票避免过度分散导致的管理精力不够。这些规则写在代码里而不是靠意志力去执行。人在开盘时会冲动程序不会。这也是量化交易跟人工交易最本质的区别规则明确、纪律坚定、没有情绪。5. 常见问题与排查技巧实录5.1 请求报错、积分不够、字段不对我用Tushare几个月最常遇到的报错基本是这几种报错信息可能原因解决办法抱歉您没有访问该接口的权限当前积分不足检查该接口的积分要求升级积分或换用其他接口接口调用太频繁频次超过限制降低调用频率加入sleep延时字段不存在字段名拼写错误或接口版本不同仔细对照官方接口文档的字段列表token不存在或已失效token未设置或过期重新生成在个人主页重新复制token并更新配置我在排查这类问题时总结了一个原则遇到报错先看接口文档再看token权限最后才检查代码。很多时候是你以为自己代码写错了其实是权限没开通在花费大量时间查代码前先做定位。5.2 性能太慢循环拼接数据怎么办最开始往本地数据库灌历史数据时我用for循环一只一只股票去拉速度惨不忍睹。几百只股票能跑一个多小时中间还容易断。后来我改成分批并发拉取用线程池控制并发数量同时对返回结果做缓冲一批攒满再统一写库。另一个优化点是使用pandas的concat一次合并多个批次数据而不是循环里逐次append后者慢得难以忍受。实际提速非常明显同样的数据量从一小时降到了几分钟。5.3 停牌、新股、退市数据老三样回测时最容易被坑的数据质量问题是停牌。停牌期间没有行情数据如果策略生成买入信号时正赶上停牌你根本买不进。我的处理方式是拿到信号后先判断当日是否可交易如果停牌就跳过。新股也要过滤因为新股上市初期波动极大且涨跌幅限制不同容易把回测结果做得虚高。退市股更麻烦数据可能在某个日期后戛然而止。这需要定期从stock_basic更新股票名单状态将退市股从可交易列表中移除。这些数据边界情况真实交易中会直接影响策略表现回测时不好好处理实盘就会给你上课。5.4 回测结果“好到离谱”时先查这两个地方有段时间我回测一个策略发现年化收益率高达500%第一反应不是高兴而是害怕。我仔细排查后果然发现两个问题一是代码里用了未来数据信号当天就成交属于典型的前视偏差二是买入时没有考虑涨停板根本买不进去可回测里我一买一个准。从此我养成了一个习惯结果越漂亮越要冷静审计。这跟做数据分析一样异常值往往是bug的信号不是市场送钱的机会。最后再分享一点个人体会这套系统从零到一跑通前后花了我大概六周时间但其中最花时间的不是写策略而是反复清洗数据、排bug、验证回测逻辑。现在回头看我踩过最深的坑都是那些“看起来很简单”的细节复权没做、未来函数没排、停牌没过滤。每一步单独拎出来都不算难题连在一起就能让一个看起来完美的策略变成一场灾难。如果你也想做自己的第一个量化系统我建议你别急着上多高深的模型先把数据链路跑通、把回测环节做扎实、把成本模型想清楚。这套基本功打扎实之后后面换策略、加因子、做风控都是一层窗户纸的事。我自己目前正在尝试把财务因子和均线策略做组合也是在这个底层框架上继续堆功能。最后给你一个实用小技巧任何一次数据拉取或回测完成后随手把结果文件和日志命名成带日期的格式。几次迭代之后你会谢我自己当初养成了这个习惯。
返回列表