☰
日频/周频量化交易策略:五个经典模型与Python实战回测详解
2026/9/29 15:32:13 网站建设 项目流程

刚接触量化那两三年,我把大量精力花在“如何更快”上面:看 tick 级数据、研究盘口挂单、试图捕捉分钟内的小波动,回测曲线看着挺热闹,一扔进实盘账户就被手续费和滑点磨得干干净净。后来我彻底想明白一件事——对于没有券商极速柜台、没有低延迟通道的个人交易者和中小团队,日频或周频的量化策略才是性价比最高的战场。

这篇文章要聊的,就是我自己反复回测、实盘验证过的 5 个经典日频/周频策略。它们都不是什么神秘黑科技,核心逻辑大多在二十年前就已经被系统化地研究过,但直到今天依然有效。我会把每个策略的适用场景、Python 实现、参数选择的原因、以及回测时最容易踩的坑都拆开讲清楚。无论你是刚开始学 Python 量化交易策略代码的新手,还是已经有一套自己的回测框架、想在策略池里补充几个稳健品种的老手,这 5 个策略都值得认认真真跑一遍。

1. 为什么我劝你从日频和周频开始,而不是死磕高频

1.1 不同频率下的“能力圈”差异

先泼一盆冷水:高频策略听起来很性感,但它的真实门槛远超多数人的想象。硬件延迟、行情快照、券商接口速度、交易费用结构,任何一个环节掉链子,你的alpha就会被吞噬。而且高频策略的容量通常很小,个人资金量进去没多久,市场冲击成本就会吃掉额外收益。

日频和周频策略恰好避开了这些硬伤。一天只做一次决策,甚至一周只做一次决策,意味着你完全不需要和机构拼速度,你拼的是逻辑的稳定性。你可以在收盘后慢慢分析数据、检查信号、写代码,然后在第二天开盘按计划执行。这种节奏下,有充足的时间思考和复盘,心态也完全不同。

1.2 日频/周频的三个现实优势

第一,数据获取成本极低。日线级别数据在众多免费数据源里都能拿到,基本不需要去买昂贵的 tick 级数据,回测的复现门槛很低。

第二,交易成本占比可控。日频策略一个月可能只交易几次,手续费和滑点对净值的影响是有限的;但如果你在分钟级别频繁进出,哪怕万几的手续费,一年下来也能吃掉好几个百分点的收益。

第三,有足够的时间做人工干预。比如消息面突变、持仓个股出现明显的基本面恶化,你可以手动调整策略参数或暂停开仓,这在日频级别是完全来得及的。

我自己目前维护的策略组合,频率最高的也就是日频,大部分仓位都是周频调仓。遇到过系统性大跌的时候,也就是因为决策频率低,反而比天天盯着盘的人拿得住。

2. 双均线趋势跟踪:最简单但绝不廉价的开场

2.1 均线交叉背后的逻辑

双均线策略应该是所有量化入门者的第一个策略:短期均线上穿长期均线做多,下穿做空。但很多人把它当作“新手玩具”放着不用,这其实是误解。

均线交叉的本质是趋势确认。短周期均线代表短期市场情绪,长周期均线代表中期持仓成本分布。当短期均线突破长期均线,说明近期的买方力量已经强于过去一段时间的平均成本,市场正在形成向上的趋势。反过来说,如果短期均线跌破长期均线,说明近期卖方力量占据主导。

选参数的时候,5/20 是最经典的一组:5 个交易日刚好是一周,20 个交易日接近一个月,分别代表了一周资金和一个月资金的成本对比。你也可以用 10/30 来降低交易频率,用 3/10 来抓更敏感的趋势。参数没有绝对优劣,关键是它要匹配你的持仓周期和可承受的回撤幅度。

2.2 代码实现与“信号平移”这个关键一步

用 Python 写一个最简版本,核心代码大概是这样:

import pandas as pd import numpy as np def dual_ma_signal(df, fast=5, slow=20): df = df.copy() df['ma_fast'] = df['close'].rolling(fast).mean() df['ma_slow'] = df['close'].rolling(slow).mean() # 信号:快线在上为1,快线在下为0 df['signal'] = np.where(df['ma_fast'] > df['ma_slow'], 1, 0) # 关键一步:信号后移一天,避免未来函数 df['position'] = df['signal'].shift(1).fillna(0) return df

这段代码里最容易被忽略的是shift(1)。为什么要平移?因为我们用当天收盘价计算出均线信号时,最早也只能在次日开盘按信号成交,如果你直接用当天的收盘价去计算收益,就等于偷偷用了未来数据,回测收益会虚高得离谱。这一步没做对,后面所有统计指标都没有意义。

回测收益的计算也很简单:

df['ret'] = df['close'].pct_change() df['strategy_ret'] = df['position'] * df['ret'] df['cum'] = (1 + df['strategy_ret']).cumprod()

双均线策略最大的风险在于震荡行情。价格来回穿越均线,会产生大量假信号,连续小亏损非常磨人。所以实际使用时,很多人会加一个过滤条件:只有当长期均线本身也在上行时才开多仓,或者用 ADX 指标判断趋势强度。这些都是后话,先把基础版本跑通,再逐步加过滤条件,是比较稳妥的学习路径。

我在实盘里的经验是,双均线策略单看胜率并不高,可能只有 35% 到 40%,但赚的时候能拿住一波趋势,亏的时候截断得也快,整体盈亏比是正的。它的价值不在于抓住每一次波动,而在于让你在市场走出大行情时保证在场。

3. 动量轮动:把“强者恒强”做成可执行的规则

3.1 动量因子为什么长期有效

如果说双均线是在单一标的上做趋势跟踪,那么动量轮动就是把趋势跟踪的思想应用到一个资产池上。核心逻辑只有一句话:过去一段时间涨得好的资产,未来一段时间大概率继续涨。

动量效应是行为金融学里被反复验证的异象之一,源于投资者的处置效应和羊群行为——人们倾向于过早卖出盈利品种、死扛亏损品种,导致价格的趋势在短期内被延续。虽然理论上有效市场假说说价格已反映一切信息,但实证数据表明,月频或周频的动量效应在绝大多数市场里长期存在。

3.2 排名、选标的与再平衡的实现

假设你有 5 个候选标的(比如不同行业 ETF 或者几只股票),动量轮动的做法是:每个调仓日计算各标的过去 N 日的涨幅,选出涨幅最高的 Top 2,等权买入,其他标的清仓。

def momentum_selection(price_df, lookback=20, top_n=2): # price_df: DataFrame,每列是一个标的的收盘价 mom = price_df.pct_change(lookback).iloc[-1] selected = mom.nlargest(top_n).index.tolist() return selected def momentum_portfolio(price_df, lookback=20, top_n=2, rebalance_days=5): # 简单模拟:每5个交易日调仓一次 positions = pd.DataFrame(index=price_df.index, columns=price_df.columns, data=0.0) for i in range(rebalance_days, len(price_df), rebalance_days): sel = momentum_selection(price_df.iloc[:i], lookback, top_n) weights = 1.0 / top_n for s in sel: positions.iloc[i, price_df.columns.get_loc(s)] = weights # 信号后移一天 positions = positions.shift(1).fillna(0) return positions

动量周期的选择上,20 日是一个比较均衡的数值,既不会因为太短而频繁交易,也不会因为太长而错失转折。如果做周频策略,也可以把 lookback 设置为 4 周(即 20 个交易日),调仓周期也是 4 周,形成“月度再平衡”的节奏。

动量轮动最容易出问题的地方是极端波动。比如某个标的过去 20 天涨了 30%,这种暴涨往往伴随着高波动,你冲进去很容易接盘。所以进阶版的动量轮动会同时考虑波动率,用“收益率除以波动率”的动量分数来排名,也就是所谓的风险调整动量。这一步能明显降低策略的回撤。

我做周频轮动时的体会是,动量因子在趋势明显的市场里非常舒服,但在快速反转的猴市里会连续挨打。解决思路不是放弃动量,而是加入一个市场状态过滤器——比如只用指数均线判断大环境,环境不好时直接空仓,等环境转好再重新启用动量轮动。

4. 布林带均值回归:抄底摸顶的正确姿势

4.1 均值回归的边界条件

如果说趋势跟踪赚的是“大行情”的钱,那均值回归赚的就是“价格偏离后回归”的钱。布林带是这个思路里最直观的工具:以 20 日均线为中轨,上下各 2 倍标准差为边界。价格触碰到上轨被认为“偏贵”,触碰到下轨被认为“偏便宜”。

但均值回归策略有一个必须承认的前提:价格并没有真正发生趋势性变化,只是短期情绪波动把它推到了极端位置。在震荡市里,这个策略表现极佳;但一旦市场进入单边趋势,抄底会变成接飞刀,摸顶会变成早下车。

所以布林带策略的第一原则不是“碰到就反手”,而是先判断市场是否处于震荡环境。实战里我会叠加一个趋势过滤器:如果中轨本身是走平的,说明市场没有明显方向,此时布林带回归才可靠;如果中轨斜率明显向上或向下,应该放弃反手信号,甚至顺着趋势方向操作。

4.2 代码实现与仓位约束

def bollinger_reversion(df, window=20, num_std=2): df = df.copy() df['mid'] = df['close'].rolling(window).mean() df['std'] = df['close'].rolling(window).std() df['upper'] = df['mid'] + num_std * df['std'] df['lower'] = df['mid'] - num_std * df['std'] # 突破下轨做多,突破上轨做空 df['signal'] = np.where(df['close'] < df['lower'], 1, np.where(df['close'] > df['upper'], -1, 0)) # 中轨走平时才启用信号 df['mid_slope'] = df['mid'].diff(5) df['filter'] = np.where(df['mid_slope'].abs() < df['std'] * 0.05, 1, 0) df['position'] = (df['signal'] * df['filter']).shift(1).fillna(0) return df

这里我额外加了一个mid_slope过滤,判断中轨在过去 5 天里是否基本走平,目的是把单边趋势行情过滤掉。

仓位约束方面,均值回归策略的单笔亏损风险天然比较低,因为你是在偏离均值较远的位置入场,止损可以设在通道外侧一点。但我非常不建议把仓位拉满,因为“便宜”不等于“不会更便宜”,极端行情下布林带可以持续扩张,价格贴着下轨连续下跌很久。所以要么设一个固定比例的止损,要么把单笔仓位控制在总资金的一小部分。

从回测数据看,布林带均值回归策略的胜率通常显著高于趋势跟踪,但单笔盈利幅度较小。它适合作为组合中的“减震器”配置,与趋势策略形成互补,而不是单打独斗。

5. RSI超买超卖:被误解最多的经典指标

5.1 RSI的算法与不同计算方式

RSI(相对强弱指标)大概是交易软件里出场率最高的指标之一,但使用者对它的误解也最多。RSI 不是简单地看“数值大于 70 就要跌,小于 30 就要涨”,它的本质是衡量一定周期内涨跌力量的对比。

RSI 的计算方式是:先算出每日涨跌幅,取上涨日的平均涨幅和下跌日的平均跌幅,两者的比值再映射到 0 到 100 区间。数值越高,说明过去一段时间买方力量越强;数值越低,说明卖方力量越占优。

传统用法是在 RSI 低于 30 时买入、高于 70 时卖出,这是一个超买超卖反转逻辑。但在实际使用中,更稳健的做法是结合市场状态来过滤信号:在震荡市里做反转,在趋势市里做顺势。具体来说,当 RSI 在 50 上方回落但不跌破 50,可以视为强势回调,继续做多;当 RSI 跌破 30 后反弹越过 30,才考虑抄底。

5.2 代码实现与趋势市/震荡市的选择

def rsi_signal(df, period=14, buy_level=30, sell_level=70): df = df.copy() delta = df['close'].diff() gain = delta.clip(lower=0).rolling(period).mean() loss = (-delta.clip(upper=0)).rolling(period).mean() rs = gain / loss df['rsi'] = 100 - 100 / (1 + rs) # 经典反转信号 df['signal'] = np.where(df['rsi'] < buy_level, 1, np.where(df['rsi'] > sell_level, -1, 0)) # 可以叠加趋势过滤:当收盘价在长期均线上方时,只做多单反转 df['ma_long'] = df['close'].rolling(60).mean() df['trend_filter'] = np.where(df['close'] > df['ma_long'], 1, 0) df['position'] = (df['signal'] * df['trend_filter']).shift(1).fillna(0) return df

这里clip(lower=0)会把负数变成 0,只保留上涨幅度;-delta.clip(upper=0)会把正数变成 0,只保留下跌幅度取正值。注意这里用的是简单移动平均处理涨跌幅,如果你想要更平滑的 RSI,可以使用 Wilder 的平滑算法,但初学阶段用简单平均更容易理解。

实战中我对 RSI 策略的体会是:它非常依赖市场的波动状态。在震荡市里,RSI 到 30 附近买入的成功率很高;但在下跌趋势中,RSI 可以长期低于 30,每次“抄底”都会变成“接刀”。所以无论你怎么设计 RSI 策略,趋势过滤永远比指标本身更重要。

RSI 另一个常见用法是背离:价格创新低但 RSI 没有创新低,说明下跌动能在衰竭。但这个信号在日频级别比较稀少,更适合作为人工复核的辅助工具,而不是单纯的规则化信号。

6. 唐奇安通道突破:海龟法则的现代简化版

6.1 为什么突破值得跟踪

海龟交易法则大概是量化史上被公开讨论最多的一套规则,它的核心工具就是唐奇安通道:创出过去 N 日新高就买入,创出过去 M 日新低就卖出。这套规则看起来简单得不像话,但它背后有很深的逻辑——价格突破新高意味着市场已经消化了所有获利盘,新的买入力量正在推动价格进入未知区域。这种突破往往伴随着趋势的启动。

日频环境下的唐奇安通道比日内突破要可靠得多,因为日线级别的假突破数量远少于分钟级别。还有一个优势是它自带止损机制:入场后,如果价格反向跌破短周期的低点,就离场。这种“突破入场 + 反向突破离场”的设计,让整个策略有明确的进出规则,不需要主观判断。

6.2 代码实现与ATR止损

def donchian_breakout(df, entry_n=20, exit_n=10, atr_n=20, atr_mult=2.0): df = df.copy() df['upper'] = df['high'].rolling(entry_n).max() df['lower'] = df['low'].rolling(entry_n).min() df['exit_lower'] = df['low'].rolling(exit_n).min() df['tr'] = np.maximum( df['high'] - df['low'], np.maximum( (df['high'] - df['close'].shift(1)).abs(), (df['low'] - df['close'].shift(1)).abs() ) ) df['atr'] = df['tr'].rolling(atr_n).mean() # 突破入场,反向突破离场 df['signal'] = 0 df.loc[df['close'] > df['upper'].shift(1), 'signal'] = 1 df.loc[df['close'] < df['exit_lower'].shift(1), 'signal'] = -1 # 持仓状态延续,直到出现反向信号 df['position'] = df['signal'].replace(0, np.nan).ffill().fillna(0).shift(1) return df

注意最后一行:signal.replace(0, np.nan).ffill()的作用是保持持仓状态,直到出现反向信号才改变仓位。如果不做这一步,策略就会变成“每天换仓”,完全违背了突破跟踪的初衷。

ATR 止损和入场是分开管理的。入场用唐奇安通道,止损用当前价格减去 2 倍 ATR。ATR 本质上是市场的平均波动幅度,用它来止损可以做到“波动大时止损宽、波动小时止损窄”,比固定百分比止损更贴合市场状态。

唐奇安通道趋势策略最大的缺点是回撤时间长。市场没有趋势的时候,价格会在通道上沿下方反复试探,触发很多次假突破,每次都很小亏损。所以在实际配置中,我只会给这个策略分配固定比例的资金,而不是把全部资金都押进去。胜率不重要,重要的是在抓到那一两次大趋势时,仓位还在,能吃到足够的利润。

7. 回测里最容易翻车的四个细节:一次完整的排查链路

7.1 未来函数:从“收益离谱”到“信号错位”

先讲一次我自己的经历。早期我用双均线策略回测一个标的,年化收益率跑出来接近 80%,最大回撤只有 6%。当时我以为发现了圣杯,结果一个前辈看了一眼代码就指出问题:我的信号没有做shift(1),等于用当天收盘价计算信号,又以当天收盘价计算收益,偷看了未来。

排查链路是这样的:

  1. 先看回测指标是否“好得不真实”:年化收益远超同策略公开报告的水平。
  2. 抽查几笔交易记录,看买入价是否恰好等于信号生成日的收盘价——这本身就值得怀疑,因为实际中你不可能在收盘那一瞬间以收盘价买入。
  3. 检查信号与收益的时间对齐方式,确认position序列在信号生成当天是否已经生效。
  4. 如果生效了,把position整体后移一天,再跑一遍,对比两份结果。

修复之后,年化收益掉到 18%,最大回撤上升到 12%。这才是合理的水平。所以说,回测代码写完以后,第一件事不是看收益,而是检查有没有未来函数。

7.2 手续费与滑点:请不要用“理想世界”骗自己

很多人回测时只按收盘价计算成交,完全忽略手续费和滑点。日频周频策略交易次数不多,这个问题会被很多人轻视,但一年累计下来,费用对净值的侵蚀仍然可观。

我自己回测时会用一组现实参数作为起步默认值:双边手续费按 0.03% 计算,滑点按 0.02% 计算,也就是每次换仓大约消耗 0.05% 的成本。对于动量轮动这种每周调仓的策略,一年调仓 50 次,光成本就会消耗掉 2.5 个百分点,如果你回测时完全不计成本,这 2.5% 就会被当成策略收益,完全是幻觉。

7.3 幸存者偏差:你的“股票池”是不是早就被时代筛选过

幸存者偏差是最隐蔽的坑。如果你用的是“当前仍然在市”的股票列表去回测过去 5 年,那些早已经退市、跌幅巨大的标的完全不会进入样本,你的回测等于天然排除了大输家。最后算出来的收益,自然好看得像天上掉馅饼。

排查方法是:看数据源是否提供了退市股票的历史数据。如果没有,至少在心里清楚一点——你回测的收益会系统性偏高。更稳妥的做法是从指数成份股的历史快照里取标的,而不是用当前成份股倒推历史。

7.4 参数过拟合:滚动窗口和样本外验证

每个策略都有参数,均线的 5/20、布林带的 20/2、RSI 的 14/30/70,这些数字不是神圣的。如果你在回测时不断微调参数,让收益曲线变得好看,那你只是在拟合历史噪声,实盘一定会现出原形。

我的习惯是把数据切成三段:前 60% 做训练,用来选参数;中间 20% 做验证,看参数是否在未参与拟合的数据段依然有效;最后 20% 做样本外测试,只跑一次,跑完就是最终结果。如果样本外表现和训练段差距明显,说明策略本身就不稳定,需要重新设计逻辑,而不是继续调参数。

8. 从回测曲线到真实账户,还差仓位和心态这两关

8.1 仓位管理:用半个凯利而不是满仓

凯利公式在纯赌博场景下可以精确给出最优下注比例,但金融市场存在不确定性分布,完全按凯利公式满仓操作,一次极端波动就可能导致毁灭性回撤。我个人的做法是:用凯利公式算出参考仓位,然后只取一半作为实际仓位,也就是所谓的“半凯利”。

简单版凯利可以用一个近似的公式估算:f* = W - (1 - W) / R,其中 W 是历史胜率,R 是平均盈利除以平均亏损的比例。比如某个趋势策略的胜率是 40%,平均盈利 / 平均亏损 = 2,那么f* = 0.4 - 0.6 / 2 = 0.1,也就是说每次只用 10% 的资金去承担风险。如果所有策略加在一起的风险敞口超过总资金的 20%,我会强制降低仓位。

8.2 回撤预案:先想好最大亏损再谈收益

实盘之前,我会为每个策略写好一份“应急预案”:策略回撤达到什么水平时降低仓位、达到什么水平时暂停交易、达到什么水平时直接下线。比如动量轮动策略连续三周亏损且累计回撤超过 8%,就先停下来重新检查市场环境是否发生了结构性变化。这套预案不是写给别人看的,是写给自己在最难受的时候看的。没有预案的人,最容易在回撤最深的底部砍仓离场,把浮亏变成实亏。

8.3 执行纪律:策略失效的识别方法与退出机制

任何一个策略都不可能永远有效。当动量因子的超额收益持续走弱、双均线的假信号频率明显增加时,要敢于承认策略进入了钝化期。我会按月记录策略的实盘表现与回测基准的偏差,连续三个月出现显著负偏差,就开始启动降仓和复盘流程。

最后再分享一个我自己的执行习惯:日频周频策略的决策次数很少,所以每一笔交易都值得认真对待。我会把下单动作固定在每天收盘后的固定时间完成,绝不盘中去追临时信号。这个习惯帮我躲过了很多次盘中冲动的交易,也让策略的回测结果和实盘结果之间,差距控制在可以接受的范围内。量化交易的本质不是预测市场,而是用规则约束人性,回测只是第一步,真正拉开差距的是后面这些“看不见”的纪律。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询