我做量化这些年,最大的感触是:很多人以为量化交易就是“写个策略然后躺着赚钱”,但真正跑通完整流程之后才发现,它更像一套“把想法变成规则、把规则变成代码、把代码放进历史数据里反复拷问”的工程方法。这篇文章就是写给刚开始接触Python量化交易的朋友,我会把从环境搭建、数据获取、第一个策略、回测框架到风险控制的全过程拆开来讲,也会把新手最容易踩的坑直接摆在你面前。文章不承诺任何收益,只讲技术流程和工程思路。
1. 量化交易到底解决什么问题
1.1 先给量化交易卸个妆
量化交易听起来高深,本质上就是一件事:把你的交易想法写成一套明确、可执行的规则,然后用历史数据去验证这套规则靠不靠谱,最后让程序按规则自动执行。
手工交易最大的问题是“不统一”。同样是看到一根大阳线,心情好的时候你觉得要追涨,心情差的时候你又觉得是诱多,同一个信号在不同时间会做出完全不同的决定。量化交易就是把这种模糊的判断变成硬性规则,比如“当短期均线上穿长期均线时买入,下穿时卖出”,然后让历史数据告诉我们,这条规则在过去到底赚不赚钱。
有人问我,量化交易是不是一定能赚钱?我的回答是:量化交易不能消除亏损,但它能把亏损变成可计算、可控制、可复盘的东西。亏钱不可怕,可怕的是不知道为什么亏、亏得没有规律。量化至少让你每一步都有据可查。
1.2 为什么偏偏是Python
市面上不是只有Python能做量化,但Python几乎成了这个圈子的默认语言。原因并不复杂。
第一,数据处理生态成熟。量化交易的核心工作是对大量历史行情数据做清洗、计算和统计,而Python的pandas、numpy这套数据工具链经过了十几年打磨,处理表格数据、时间序列数据非常顺手。你用Excel处理几千行数据还能忍受,但一旦面对上万行、上百个字段的行情数据,Excel根本跑不动。
第二,开发和验证的迭代速度快。量化策略研究的常态是“快速试错”,一个想法能不能成立,先用历史数据跑一遍再说。Python的语法灵活,写一个策略原型可能只需要几十行代码,改参数、改逻辑的成本很低。相比之下,用C++做这类探索性工作,光编译和内存管理就能消耗掉大量时间。
第三,社区和周边工具足够丰富。从行情数据接口、回测框架到机器学习库、券商交易接口,Python生态里几乎都能找到现成方案。这意味着你不用从零造轮子,可以把精力集中在策略逻辑本身。至于那些说Python慢的,确实在高频交易场景下Python不是首选,但对绝大多数中低频策略而言,Python的性能完全够用。量化交易比拼的从来不是谁的语言更快,而是谁的逻辑更经得起推敲。
1.3 开始之前,先确认自己准备好了
写代码之前,我想先泼一盆冷水。如果你Python基础还不牢,连DataFrame切片都写不顺,那建议先不要急着碰策略,先把pandas基础补一补。不需要成为专家,但下面这几个能力是底线:
- 能读懂常见的Python语法,包括函数、类、列表推导式
- 会用pandas读取CSV、按行按列筛选数据、计算滚动均值
- 理解什么是K线、开盘价、收盘价、成交量,理解均线的基本含义
如果你这些基础都还没有,硬着头皮写策略只会让你陷入“Bug和策略逻辑混在一起”的泥潭。哪怕策略思路是对的,因为代码错误带来的错误结果,也会让你对策略产生完全错误的判断。所以我的建议是:先花一两周时间把Python和pandas的基础操作练熟,再回来学量化。基础不牢,后面每一步都会很痛苦。
2. 搭一套本地开发环境:三步走
2.1 用虚拟环境隔离你的依赖
很多初学者一上来就在全局环境里装库,结果装的库多了之后,版本冲突会让人非常头疼。比如某个库需要pandas 1.x,另一个旧项目又锁死pandas 0.25,互相覆盖之后,之前能跑的代码突然就不能跑了。
我建议你从最开始就养成用虚拟环境的习惯。具体做法很简单,安装Miniconda(一个轻量级Python环境管理工具),然后创建一个独立的量化环境:
conda create -n quant python=3.10 -y conda activate quant接着在这个环境里安装最基础的一批库:
pip install pandas numpy matplotlib backtrader这里我把要装的库控制在最少:pandas负责数据处理,numpy负责数值计算,matplotlib负责画净值曲线,backtrader是一个开源的回测框架,后面我们会用到。等以后需要机器学习或者统计功能时,再按需安装scikit-learn、statsmodels这些库,而不是一次全部装上去。
2.2 行情数据从哪里来
这是新手问得最多的问题。我的建议是:不要一上来就接实时行情或者券商接口,先找一份历史日线数据,用CSV文件把流程跑通。
CSV是纯文本格式,任何工具都能打开,也没有平台依赖。一个标准的日线数据文件,通常至少包含日期、开盘价、最高价、最低价、收盘价、成交量这几列。你可以从自己熟悉的行情软件里导出数据,也可以在开源社区找一些免费的历史数据接口下载。
这里想提醒一句:数据源的稳定性和合规性一定要自己把关。很多免费接口过一段时间就会失效,或者数据字段发生变化,这是常态。我的习惯是把数据下载后先存成CSV放本地,做策略分析时直接读本地文件,不依赖实时网络,这样既能保证数据一致,也能避免接口波动干扰策略开发。
读取CSV的代码很简单:
import pandas as pd df = pd.read_csv("stock_daily.csv") df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() print(df.head())把日期列转换成时间类型,然后设为索引,这一步很关键。因为后续所有的均线计算、收益率计算、回测逻辑都依赖“按时间顺序排列”和“时间索引对齐”,如果日期没有解析成时间类型,很多运算都会出错。
2.3 拿到数据先做三件事
不管是下载的数据还是自己导出的数据,我都建议先做一遍体检,再开始写策略。检查三个地方:
第一,看缺失值。行情数据里出现NaN很常见,尤其是停牌的日子。处理方式要根据情况来,如果缺口很小,可以直接删除;如果像除权除息导致的价格跳空,可能要结合复权数据一起处理。
print(df.isna().sum())第二,看数据范围。用df.describe()能看到每列的最小值、最大值、均值,如果发现某天的收盘价是0或者负数,这种脏数据必须清理,否则均线计算会直接崩掉。
第三,看索引是否有序且无重复。重复日期会直接影响收益率计算,排序错误会影响信号生成。用这个代码可以快速检查:
print(df.index.is_monotonic_increasing) print(df.index.duplicated().sum())这三步做完,数据干净了,后面写策略才不会莫名奇妙跑出离谱结果。
3. 第一个策略:从想法到代码
3.1 为什么第一策略选双均线
双均线策略是我推荐给所有新手的第一个策略。不是因为它能稳定赚钱,而是因为它足够简单,能让你把“策略思路、数据处理、信号生成、回测评估”这条完整链路跑通。
双均线的逻辑很直白:分别计算短期均线和长期均线,当短期均线从下往上穿过长期均线时,说明短期趋势转强,产生买入信号,这就是所谓的“金叉”;当短期均线从上往下跌破长期均线时,说明短期趋势转弱,产生卖出信号,这就是“死叉”。
比如我们可以用5日均线代表短期趋势,用20日均线代表长期趋势。5日均线反映最近一周的平均价格,20日均线反映最近一个月的平均价格。短期均线突破长期均线,可以理解为“最近的价格涨势已经超过了更长周期的平均成本”,市场情绪偏多。
3.2 手把手写信号生成:不要栽在未来函数上
下面这段代码,就是完整的双均线信号生成:
df["sma_short"] = df["close"].rolling(window=5).mean() df["sma_long"] = df["close"].rolling(window=20).mean() df["signal"] = 0 df.loc[df["sma_short"] > df["sma_long"], "signal"] = 1 df.loc[df["sma_short"] < df["sma_long"], "signal"] = -1 df["position"] = df["signal"].shift(1)这里的sma_short是5日均线,sma_long是20日均线。当短期均线大于长期均线时,我们把信号标记为1,表示“看多”;反过来标记为-1,表示“看空”。
注意最后一行:df["signal"].shift(1)。这行代码极其重要,也是新手最容易漏掉的地方。
为什么需要shift(1)?因为你在计算当天的均线时,用的已经是当天的收盘价。如果当天收盘价上穿均线,你当天就给信号、当天就持仓,这在回测里等于“用当天收盘价的信息,按当天收盘价成交”,现实中根本做不到。更合理的假设是:当天收盘后确认信号,第二天开盘再执行交易。所以要把信号向后平移一天,让策略实际持仓从信号出现的后一天才开始。
没有这行代码,你的回测结果会异常漂亮,因为它“偷看”了未来。这就是量化里常说的未来函数,也是绝大多数人回测好看、实盘翻车的第一大原因。
3.3 手动计算净值曲线:给策略打分
有了持仓信号,接下来计算策略的每日收益率:
df["daily_return"] = df["close"].pct_change() df["strategy_return"] = df["position"] * df["daily_return"] df["strategy_net"] = (1 + df["strategy_return"]).cumprod() df["benchmark_net"] = (1 + df["daily_return"]).cumprod()pct_change()计算的是每个交易日相对前一天的涨跌幅。position等于1的日子,策略就完全跟随涨幅;等于-1的日子,策略收益就是负的当日涨跌幅,也就是做空的意思。
cumprod()是把每天的收益率累乘起来,得到一条净值曲线。strategy_net是策略的净值,benchmark_net是单纯买入持有不动(或者按某指数计算)的基准净值。有了这两条曲线,你就可以直观地看到,自己的策略到底有没有跑赢“什么都不做”。
跑完这20行代码,恭喜你,已经完成了一个最简单策略从数据到评估的完整闭环。接下来要做的,是把这个流程搬进一个更规范的回测框架里。
4. 回测框架与评估指标:真的会看结果比会写代码更重要
4.1 手写回测和框架回测的差别
上面我们手工计算的净值曲线,本质上就是一种最简单的向量化回测。它的优点是简单直接,但缺点是它过于理想化,不考虑手续费、滑点、成交限制、资金约束,也不方便处理复杂的下单逻辑。
如果只是验证一个粗略的想法,向量化回测够用了。但如果你想认真研究策略,我建议切换到一个规范的回测框架。backtrader是社区里比较经典的开源回测框架,它把交易流程里的很多细节都考虑进去了:手续费、滑点、分批下单、仓位管理、多数据源支持等,而且通过事件驱动的方式模拟交易流程,更接近真实交易环境。
4.2 用backtrader跑一遍双均线策略
下面这段是backtrader的极简示例,逻辑和前面完全一致:
import backtrader as bt class DualSMA(bt.Strategy): params = (("short_window", 5), ("long_window", 20),) def __init__(self): self.sma_short = bt.indicators.SimpleMovingAverage( self.data.close, period=self.params.short_window) self.sma_long = bt.indicators.SimpleMovingAverage( self.data.close, period=self.params.long_window) self.crossover = bt.indicators.CrossOver(self.sma_short, self.sma_long) def next(self): if not self.position: if self.crossover > 0: self.buy() elif self.crossover < 0: self.close() cerebro = bt.Cerebro() data = bt.feeds.GenericCSVData( dataname="stock_daily.csv", dtformat="%Y-%m-%d", datetime=0, open=1, high=2, low=3, close=4, volume=5, openinterest=-1, ) cerebro.adddata(data) cerebro.addstrategy(DualSMA) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission=0.001) print("初始资金: %.2f" % cerebro.broker.getvalue()) cerebro.run() print("期末资金: %.2f" % cerebro.broker.getvalue())这段代码的核心逻辑在next方法里。self.crossover > 0表示短期均线刚刚上穿长期均线,也就是金叉;self.crossover < 0表示死叉。没有持仓的时候出现金叉就买入,持有仓位时出现死叉就卖出。
setcommission设置的是手续费比例,这里设的是0.1%。这个参数一定要设,因为真实交易中手续费和滑点会直接影响策略收益,忽略这些成本会严重高估策略表现。
4.3 评估指标怎么看:别被高收益冲昏头
回测跑完之后,backtrader会输出期末资金。但只看期末资金远远不够,必须看几个核心指标。
最大回撤是最重要的风险指标。它表示从净值最高点到之后最低点的最大跌幅。打个比方,100万本金亏到50万,最大回撤就是50%,而之后要从50万赚回100万,需要涨100%。回撤越大,把你的账户拖入深渊的难度就越大,所以回撤必须优先关注。
年化收益率和最大回撤配合起来看才有意义。一个年化20%、最大回撤5%的策略,远比年化40%、最大回撤50%的策略更值得重视。
夏普比率衡量的是“每承担一单位风险,能换来多少超额回报”。简化的计算方法是:策略收益率减去无风险利率,再除以策略收益率的波动率。夏普比率大于1算勉强及格,大于2就算很理想了。要注意的是,夏普比率在数据跨度短、波动率异常时参考价值有限,不能唯指标论。
胜率和盈亏比是另一个维度。胜率表示盈利交易笔数占总交易笔数的比例,盈亏比表示平均盈利金额与平均亏损金额的比值。胜率低不要紧,只要盈亏比足够高,策略仍然可能赚钱;反过来,胜率很高但每次赚一点点、亏起来亏很多,这种策略长期很难活下去。
这几个指标的重要性排序,我的看法是:最大回撤大于年化收益,夏普比率大于胜率。先保证策略不会让你亏穿,再谈赚多少。
5. 风险与资金管理:先活下来,再谈盈利
5.1 回测赚钱,实盘为什么亏钱
很多人把回测结果直接当作实盘预期,这是最大的误区。回测和实盘之间有四个主要的“摩擦成本”:
滑点。回测默认你按收盘价或者信号价格成交,但真实市场的买一卖一价差和订单冲击会让你的实际成交价永远比预期更差一点。流动性越差的品种,滑点越严重。
手续费。每一笔交易都要交手续费,如果是股票交易,卖出还要交印花税。每次交易成本看似不高,但策略交易频率一高,复利效应下成本会非常惊人。
成交限制。回测里信号出现了就能成交,但现实中可能遇到一字涨停买不进、一字跌停卖不出的情况,尤其在小盘股上,这个问题非常突出。
心态变化。回测时面对的是曲线,实盘时面对的是真金白银的波动。浮亏30%的时候,很少人能淡定地继续按规则交易,而一旦中途放弃规则,回测就彻底失效了。
我的建议很朴素:任何策略在上实盘之前,先按“回测收益打个五折、回撤放大一倍”的心理预期去做压力测试。如果打折后你仍然能接受,再考虑实盘。
5.2 仓位管理:单笔风险控制在总资金的2%以内
仓位管理的方法很多,但新手最容易犯的错误是“一把梭”。满仓交易最大的问题是:连续几次亏损之后,本金快速缩水,心态崩溃,后续所有的策略执行都会变形。
我推荐一套最简单也最通用的做法:单笔风险不超过总资金的2%。这里的“风险”不是指投入的仓位大小,而是指“如果这笔交易止损,亏损金额占总资金的比例”。
举个例子,你总资金10万元,单笔风险设为2%,也就是单笔最多亏2000元。假设某笔交易入场价是50元,止损价是48元,每股亏损2元,那么最大买入数量就是2000除以2,等于1000股。这样即使连续亏损5笔,总亏损也只有10%,账户仍然有足够的底气继续执行策略。
计算公式写成Python就是:
total_capital = 100000 risk_per_trade = 0.02 entry_price = 50 stop_loss_price = 48 risk_amount = total_capital * risk_per_trade max_shares = int(risk_amount / (entry_price - stop_loss_price)) print(max_shares)很多人喜欢用凯利公式计算仓位,觉得它能最大化长期增长率。但凯利公式要求你对胜率和盈亏比有相当准确的估计,而策略的这两个参数本身就不稳定,用不准确的参数去套凯利公式,结果往往比固定比例仓位更危险。对新手来说,固定比例仓位的稳定性比理论最优更重要。
5.3 过拟合:最容易自我欺骗的环节
做策略研究时,有一个极其常见的陷阱:反复调整参数,直到回测曲线变得非常漂亮。比如双均线策略,你不断尝试5日和20日、10日和30日、8日和26日,最终挑出一组让历史回测收益最高的参数。这看起来像是在“优化策略”,实际却是在“让策略背诵历史答案”。
量化圈有一句话说得很直白:对历史数据解释得越完美,对未来的预测能力往往越差。因为市场数据里有大量噪声,参数优化过度时,策略学到的是噪声模式,而不是真实规律。
防止过拟合有几个笨但有效的办法。第一,把数据分成两段,一段叫样本内数据,用来开发策略和调整参数;另一段叫样本外数据,用来做最终验证。如果策略在样本外数据上表现严重退化,说明策略本身就是过拟合的。第二,减少参数数量。参数越少,策略的“自由度”越低,过度适配历史的可能性越小。第三,做参数稳定性检验,比如把均线周期改成7日和22日,或者4日和19日,看策略收益是否发生了剧烈变化。如果参数稍微一动结果就从赚钱变成亏钱,这个策略基本不可信。
6. 新手最常踩的坑:问题排查速查表
6.1 数据文件里的隐形炸弹
我在带新人时发现,回测结果越离谱(比如年化百分之几百),问题往往不在策略,而在数据。
复权问题是最常见的坑。股票分红送股之后,股价会跳空,如果不复权,K线上会出现虚假的巨大跌幅,均线计算也会有偏差。前复权保证历史价格信号与当前价格衔接平滑,适合看盘和技术分析;后复权则更适合计算真实的收益率。关键是要保证整个回测过程统一用一种复权方式,不要混用。
停牌日和涨跌停日也会制造假信号。停牌期间没有成交,成交量是0,但很多数据接口会把价格填成停牌前一天的数值。而一字涨停的时候,你回测里可以成交,现实中根本买不进去。这些异常值如果不在数据清洗阶段处理掉,回测结果会非常失真。
这里整理了一张常见数据问题排查表:
| 异常现象 | 可能原因 | 检查方式 |
|---|---|---|
| 某天收益率异常大 | 未复权导致跳空 | 检查当日价格是否连续 |
| 成交量出现大量0 | 停牌或数据缺失 | 查看成交量最小值 |
| 日期索引重复 | 数据源合并错误 | duplicated().sum() |
| 收盘价为负 | 数据解析错误 | describe()查看min |
6.2 代码逻辑里的经典Bug
回测代码的Bug往往不会直接报错,而是悄悄让结果失真。下面这几个是我见过最高频的问题。
第一个是未来函数。前面已经强调过,信号当天产生、当天成交,在回测里属于“偷看未来”。所有决策和成交之间,至少要留出一个时间间隔(比如信号次日执行)。
第二个是持仓与收益率的错位。如果你用position和当天的daily_return相乘,必须保证position是“当天开盘时实际持有的状态”,所以要处理shift的问题。不少人把signal和signal.shift(1)搞混,导致持仓信号和收益率方向错位一天,造成结果失真。
第三个是链式赋值的隐患。在pandas里,不要用df[df["close"]>10]["signal"] = 1这种方式给DataFrame赋值,它会触发链式赋值的警告,赋值结果有时能生效有时不能。正确写法是df.loc[df["close"]>10, "signal"] = 1。
6.3 实盘衔接的系统性问题
从回测走向实盘,还有一堆工程化问题。比如实盘API的网络断线重连,程序崩溃后的重启机制,交易日志的记录,策略信号的持久化等等。这些不属于“策略问题”,但每一个都可能让实盘中执行结果和回测产生巨大偏差。
我见过不少人回测跑得很顺,实盘第一天就亏在程序逻辑上:漏单、重复下单、行情源延迟导致信号错误。所以我强烈建议,实盘之前至少先跑一段时间模拟盘,让程序在真实行情下连续运行,确认下单、撤单、持仓更新这些环节都稳定了,再考虑投入真金白银。
在这些工程问题确认之前,不要轻易把实盘当“验证工具”。程序本身不稳定的时候,你根本分不清亏钱是因为策略错了,还是因为代码错了。
做量化交易这几年,我越来越觉得,写策略代码只是其中一小部分,真正花时间的地方在于数据清洗、指标分析、风险控制和反思复盘。Python给我的最大价值,不是让我找到一个稳赚的公式,而是逼着我用一种有纪律、可验证的方式去面对市场。如果你刚开始走这条路,我建议你先跑通双均线这个完整流程,把环境、数据、回测、评估每一个环节都亲手过一遍,先别急着追求高收益,先让自己拥有一套可信的验证方法和健康的风险习惯。这套基本功建立起来之后,后面研究任何新策略,你都会觉得从容很多。