先交代一下背景:大概半年前,我还是一个每天打开行情软件盯分时图、靠直觉买卖的散户。后来被一个做私募的朋友刺激到了,人家做交易靠的不是盘感,是系统。于是我也决定搞一套自己的“系统”,但真动手才发现,做量化最难的不是写策略,而是搞数据。国内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(exchange='SSE', start_date='20200101', end_date='20250101') print(calendar.head())交易日历是整个系统的时间基准,因为A股有休市、调休,直接用自然日去推断交易日会出大问题。比如周一虽然是工作日,但遇到节假日就休市,如果你按自然日去拉行情,就会拉到空数据。交易日历就是用来规避这个问题的。
紧接着拉一份当前在市的所有股票列表:
# 获取股票基础信息,list_status='L'代表上市状态 stock_list = pro.stock_basic(exchange='', list_status='L', fields='ts_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_code='000001.SZ', start_date='20240101', end_date='20250201') 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(window=5).mean() df['ma20'] = close.rolling(window=20).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_cash=1000000): 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:30,13: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、验证回测逻辑。现在回头看,我踩过最深的坑都是那些“看起来很简单”的细节:复权没做、未来函数没排、停牌没过滤。每一步单独拎出来都不算难题,连在一起就能让一个看起来完美的策略变成一场灾难。
如果你也想做自己的第一个量化系统,我建议你别急着上多高深的模型,先把数据链路跑通、把回测环节做扎实、把成本模型想清楚。这套基本功打扎实之后,后面换策略、加因子、做风控都是一层窗户纸的事。我自己目前正在尝试把财务因子和均线策略做组合,也是在这个底层框架上继续堆功能。
最后给你一个实用小技巧:任何一次数据拉取或回测完成后,随手把结果文件和日志命名成带日期的格式。几次迭代之后,你会谢我自己当初养成了这个习惯。