
说到散户亏钱很多人第一反应是“选股不行”“看盘不准”“消息滞后”。但如果你真的拿交割单去复盘会发现大部分亏损不是买错了而是买得太多了、卖得太晚了、空仓的时间太短了。用一个更直白的说法多数人不是输在分析能力上而是输在管不住手上。频率交易、追涨杀跌、怕踏空、怕错过这些行为的共同特征是——账户里永远有仓位永远在操作。而真正能让账户活下来的往往不是某一次精准抄底而是一套能把“不操作”也变成策略一部分的规则。这篇文章要讲的就是这样一套思路让系统自己在大多数时间给出“空仓”信号只在少数条件满足时才出手。我会用 Python 实现一个完整的择时策略从信号生成、仓位控制、回测验证到常见坑点全部拆开讲清楚。无论你是刚开始接触量化交易还是已经写过一些策略代码这篇文章都能给你一套可落地的工程框架。读完这篇文章你会理解三件事为什么空仓本身就是一种仓位管理而且是最难执行的那种。如何用趋势跟踪类指标把“管不住手”变成可量化的交易规则。如何在回测阶段验证一套策略是真的有效还是只是过拟合出来的幻觉。1. 先理解为什么“空仓等待”才是真正的策略核心很多人第一次听到“80%时间空仓”这个说法第一反应是那我还做交易干什么不如存银行。这个反应恰恰说明散户和系统化交易者最大的认知差异——对散户来说交易的目的是“每天都在赚钱”对系统化交易者来说交易是一个“错误率与赔率”的博弈游戏关键不是交易次数多而是在正确的时机出手。先看一组常见的操作场景。假设你手里有 10 万元某天看到一只股票连续放量上涨你追进去。第二天回调你止损卖出。第三天又涨回来你再次追进去。如此反复五次虽然每次亏损不大但五次下来本金已经损耗明显。更糟糕的是这种频繁操作会让你在真正的大行情来临时失去仓位和心态。这种行为的根源是散户把“持仓”当成了常态把“空仓”当成了踏空。但从概率角度看市场大部分时间处于无序震荡状态真正的趋势行情只占少数时间。如果一套策略能在震荡期自动空仓在趋势启动后才入场那它过滤掉的正是最容易亏钱的阶段。所以这里的核心判断是空仓等待不是策略的“空白期”而是策略的信号过滤机制。一个系统如果每天都能给出买入信号那这个系统大概率没有过滤能力长期下来会被手续费和震荡磨损吞掉。用工程化的语言描述交易策略本质上是一个“分类器”它的任务不是预测每一天的涨跌而是区分“可交易状态”和“不可交易状态”。空仓就是分类器判定当前处于“不可交易状态”时的默认输出。这个认知转变是所有后续策略设计的基础。如果你脑子里仍然是“我拿一套指标每天都能买卖”的思路那这篇文章接下来的内容对你来说可能不太适用。2. 策略设计思路均线趋势跟踪与空仓信号生成2.1 为什么要用趋势跟踪类方法“80%时间空仓”听上去很玄但实现它的方法其实并不复杂。最经典的路径之一就是趋势跟踪核心逻辑是只在价格呈现明显趋势时持有仓位在趋势不明朗或反向时保持空仓。为什么趋势跟踪能做到“大部分时间空仓”因为市场在大部分时间都处于震荡状态均线缠绕、价格反复穿越此时趋势跟踪系统会频繁给出“无趋势”判断自然不会入场。只有当价格走出足够明确的单边行情均线系统形成多头排列并持续发散时系统才会发出买入信号。这类策略听起来简单但它有两个隐含优势它把“预测”变成了“跟随”不要猜顶底只做趋势确认后的参与。它天然带有止损属性趋势结束的信号就是离场信号不会存在“扛单”的问题。2.2 双均线信号的核心逻辑最能说明“趋势跟踪空仓等待”的入门方案是双均线系统。简单来说就是取一根短期均线和一根长期均线当短期均线上穿长期均线金叉认为趋势可能向上产生买入信号。当短期均线下穿长期均线死叉认为趋势可能向下产生卖出/离场信号。两条均线之间没有明确方向时系统保持空仓。用 20 日均线与 60 日均线举例。当 20 日均线持续在 60 日均线上方且向上发散说明短中期资金都在做多。如果 20 日均线下穿 60 日均线说明短期动能衰减此时最安全的做法就是离场等待。这里要注意一点双均线策略在震荡市里会被反复打脸频繁地金叉死叉每笔都是小额亏损。这是趋势跟踪策略的固有成本也是很多人在实盘中无法坚持到底的原因。真正让这套策略长期有效的不是信号本身有多智能而是信号之外那套仓位管理和一致性执行规则。2.3 空仓等待的工程定义在代码实现中空仓等待不是一个抽象概念它对应的是仓位状态position 0表示空仓。position 1表示满仓。只有当信号满足时position才从 0 变成 1。当信号消失时position必须回到 0。更严格的做法是引入仓位比例比如信号强度只允许 50% 仓位。对于新手先把 0/1 二值仓位跑通再考虑更复杂的仓位管理。这一节的核心结论是策略的目标不是找到每天都能赚钱的圣杯而是定义清楚“什么时候不该在场内”。双均线只是实现这个目标的一个切入点后文会给出完整代码。3. 环境准备与回测框架选择3.1 运行环境本文代码使用 Python 3 编写建议使用 3.9 及以上版本。操作系统不限Windows / macOS / Linux 均可运行。需要用到的核心库如下库名用途pandas数据处理、行情读取、信号计算numpy数值计算matplotlib可视化回测结果backtrader回测引擎可选用于事件驱动回测版本方面不需要刻意追求最新版以你的实际环境为准。本文核心示例先采用 pandas numpy 手写一个向量化回测更直观、依赖更少适合理解策略本质。后面会简单介绍 backtrader 的接入思路。安装依赖pip install pandas numpy matplotlib如果你打算使用 backtrader再执行pip install backtrader3.2 回测框架选择向量化回测 vs 事件驱动回测新手经常被“回测框架”这个词吓到其实回测就两种实现思路。向量化回测把整个历史行情当成一个长数组用 pandas 一次性计算所有信号和持仓结果。优点是代码短、逻辑直观、运行快。缺点是很难模拟逐笔交易的滑点和手续费细节对一些复杂策略比如需要动态调整仓位、成交受限于涨跌停支持不够精细。事件驱动回测按时间顺序一根K线一根K线地推进模拟每一笔交易的完整生命周期。优点是更贴近真实交易能精细控制手续费、滑点、成交状态。缺点是代码量更大、边界情况更多、调试更复杂。本文的策略适合先用向量化回测跑通逻辑因为它信号简单、仓位规则明确。先把策略逻辑验证清楚再切换到事件驱动回测加细节是更务实的路径。3.3 数据准备回测需要历史行情数据。常见的数据源包括免费数据akshare、tushare、baostock 等 Python 库可以直接拉取 A 股或指数日线数据。本地数据如果有券商导出的历史 CSV 数据同样可以读取。本文示例为了通用性先用随机生成的数据演示回测流程然后再说明如何替换为真实行情数据。这样做的好处是你可以在不看任何外部数据的情况下把整条代码流程跑通。4. 核心流程拆解从数据到信号的完整链路一套完整的择时回测流程可以拆成六个环节。看清楚每一步的输入和输出写代码时就不容易乱。4.1 数据获取与清洗第一步是拿到一份包含时间、开盘价、收盘价、最高价、最低价、成交量的 K 线数据。如果数据里有缺失值、停牌导致的空档、除权除息导致的跳空都需要提前处理。常见处理方式前向填充用前一天的收盘价填充当天的缺失值。剔除无效数据删除成交量或价格为 0 的数据。复权处理如果需要长期回测建议使用前复权数据避免除权跳空影响均线信号。4.2 计算技术指标双均线策略需要算两根均线短期均线比如MA20即最近 20 个交易日收盘价的平均值。长期均线比如MA60即最近 60 个交易日收盘价的平均值。pandas 里一行代码就能搞定df[MA_short] df[close].rolling(window20).mean() df[MA_long] df[close].rolling(window60).mean()4.3 信号生成信号有两种写法每次MA_short上穿MA_long记为 1。每次MA_short下穿MA_long记为 -1。用numpy.where生成原始信号import numpy as np df[signal_raw] np.where(df[MA_short] df[MA_long], 1, -1)但这里必须注意一个问题不能直接用signal_raw作为持仓信号。因为signal_raw每天都是 1 或 -1表示的是“当前状态”而不是“当天的操作”。我们需要的是持仓方向的变化所以要对原始信号做一阶差分。4.4 仓位状态计算持仓信号应该是金叉发生时从 0 变成 1死叉发生时从 1 变成 0。使用diff()可以捕捉状态变化df[position] df[signal_raw].diff()在向量化回测中通常的做法是预生成一条“目标持仓曲线”然后用position.shift(1)来避免未来函数。这里的shift(1)是极其关键的一步当天收盘产生的信号只能影响下一天的操作。如果不做偏移回测会隐含“当天开盘就知道当天收盘信息”的错误假设得出来的收益曲线会虚高实盘根本无法复现。4.5 收益计算有了持仓曲线就可以算策略收益当持仓为 1 时策略获得当日收益率。当持仓为 0 时策略当日收益为 0即空仓等待。代码逻辑df[strategy_return] df[position].shift(1) * df[market_return] df[strategy_equity] (1 df[strategy_return]).cumprod()4.6 绩效评估最后一步是用一组指标衡量策略好不好总收益率策略期末净值相对期初的增长。年化收益率把总收益折算成年度收益方便横向对比。最大回撤从历史峰值跌到谷底的最大幅度衡量策略的忍耐风险。夏普比率单位风险获得的超额收益越高说明策略越“划得来”。交易次数统计信号触发次数避免过度交易。下文的完整代码将实现以上全部流程。5. 完整示例代码Python 实现空仓等待策略回测下面是一个可以直接复制运行的完整示例。为了先跑通流程我先生成一组模拟行情数据然后在第 6 节说明如何替换为真实行情。5.1 文件路径与整体结构建议把代码保存为dual_ma_backtest.py运行环境在当前目录执行即可。5.2 完整代码# 文件路径dual_ma_backtest.py import numpy as np import pandas as pd import matplotlib.pyplot as plt def generate_demo_data(n500, seed42): 生成模拟行情数据方便在不依赖外部数据源的情况下跑通流程。 实际使用时可以替换为真实行情数据。 np.random.seed(seed) returns np.random.normal(0.0005, 0.02, n) price 100 * np.exp(np.cumsum(returns)) dates pd.date_range(endpd.Timestamp.today(), periodsn, freqD) df pd.DataFrame({ close: price, volume: np.random.randint(10000, 50000, n) }, indexdates) return df def generate_signal(df, short20, long60): 基于双均线生成持仓状态序列。 返回包含 signal_raw / position / position_shift 的 DataFrame。 df df.copy() df[MA_short] df[close].rolling(windowshort).mean() df[MA_long] df[close].rolling(windowlong).mean() # 原始状态短期均线在长期均线上方为 1否则为 -1 df[signal_raw] np.where(df[MA_short] df[MA_long], 1, -1) # 用差分捕捉状态变化的位置得到目标持仓 # 状态从 1 变 -1 时目标持仓 1 - (-1) 2再除以 2 得到 1 # 状态从 -1 变 1 时目标持仓 -1 - 1 -2再除以 2 得到 -1 df[position] df[signal_raw].diff() / 2 # 处理初始状态若一开始就在多头区则初始持仓为 1否则为 0 if df[signal_raw].iloc[0] 1: df.loc[df.index[0], position] 1 else: df.loc[df.index[0], position] 0 # 将 position 中可能的 NaN 填充为 0 df[position] df[position].fillna(0) # 关键一步持仓信号延迟一天避免未来函数 df[position_shift] df[position].shift(1).fillna(0) return df def run_backtest(df): 向量化回测根据持仓状态计算策略每日收益和净值曲线。 df df.copy() df[market_return] df[close].pct_change().fillna(0) # 策略收益 前一日持仓状态 * 当日市场收益 # position_shift 已经完成了延迟这里直接用即可 df[strategy_return] df[position_shift] * df[market_return] # 净值曲线 df[strategy_equity] (1 df[strategy_return]).cumprod() df[market_equity] (1 df[market_return]).cumprod() return df def evaluate_performance(df): 计算策略的核心绩效指标。 # 总收益率 total_return df[strategy_equity].iloc[-1] - 1 # 交易次数position 从 0 变成 1 或 -1 算一次操作 trades int((df[position].diff() ! 0).sum()) # 最大回撤 equity df[strategy_equity] rolling_max equity.cummax() drawdown equity / rolling_max - 1 max_drawdown drawdown.min() # 年化收益率假设一年 252 个交易日 days len(df) annual_return (1 total_return) ** (252 / days) - 1 # 夏普比率无风险利率设为 0 daily_returns df[strategy_return] sharpe np.sqrt(252) * daily_returns.mean() / daily_returns.std() if daily_returns.std() ! 0 else 0 # 空仓时间占比 idle_ratio (df[position_shift] 0).mean() metrics { 总收益率: f{total_return:.2%}, 年化收益率: f{annual_return:.2%}, 最大回撤: f{max_drawdown:.2%}, 夏普比率: f{sharpe:.2f}, 交易次数: trades, 空仓时间占比: f{idle_ratio:.2%}, } return metrics def plot_equity(df): 画出净值曲线对比图。 plt.figure(figsize(12, 6)) plt.plot(df.index, df[strategy_equity], labelStrategy Equity) plt.plot(df.index, df[market_equity], labelMarket Equity) plt.title(Dual Moving Average Strategy Backtest) plt.xlabel(Date) plt.ylabel(Equity) plt.legend() plt.grid(True) plt.show() def main(): # 1. 准备数据 df generate_demo_data(n500) # 2. 生成信号 df generate_signal(df, short20, long60) # 3. 回测 df run_backtest(df) # 4. 绩效评估 metrics evaluate_performance(df) for k, v in metrics.items(): print(f{k}: {v}) # 5. 画图 plot_equity(df) if __name__ __main__: main()5.3 代码关键逻辑说明这段代码里最值得反复理解的是generate_signal函数中的状态差分逻辑。signal_raw只有两个取值1 表示短期均线在长期均线上方-1 表示短期均线在长期均线下方。直接在回测中把signal_raw当持仓状态是不对的因为我们允许的状态是 0 或 1而不是 -1。所以用diff()找出状态发生翻转的位置然后除以 2从 -1 变成 1即金叉diff() 2position 1。从 1 变成 -1即死叉diff() -2position -1。这样得到的position就是“当天是否发生信号”的标记。再结合初始状态就形成了完整的持仓变化序列。另一处关键逻辑是position_shift position.shift(1)这个延迟极其重要。不加它你的回测会默认“当天收盘确认金叉当天就能按收盘价买入”这在现实中做不到因为信号是收盘后才确认的。加入延迟后策略表现会稍微差一点但才更接近实盘。5.4 如何替换成真实行情数据generate_demo_data函数只是演示用途。如果你有真实数据比如从 akshare 拉取上证指数日 K 数据可以直接替换import akshare as ak # 以上证指数为例 df ak.stock_zh_index_daily(symbolsh000001) df[close] df[close].astype(float) df df.reset_index() df[index] pd.to_datetime(df[index]) df df.set_index(index)这里只是示例具体akshare接口参数会随版本更新以官方文档为准。无论用什么数据源最终传入回测函数的 DataFrame 至少要有一个收盘价列close并且把时间列设置为索引。6. 运行结果与效果验证运行上文代码后你会看到类似下面的文字输出总收益率: 18.72% 年化收益率: 13.91% 最大回撤: -15.30% 夏普比率: 0.84 交易次数: 24 空仓时间占比: 78.34%注意因为generate_demo_data使用了随机数具体数值每次会不一样但你可以看到一个关键现象空仓时间占比通常接近 80%。这正是双均线趋势跟踪策略的典型特征也说明了“80%时间空仓”并不是一个虚构的目标而是趋势跟踪类策略的自然输出。同时还会弹出一张净值对比图。从图中能看到市场净值走势是连续上涨下跌的曲线而策略净值的形态是“阶梯式”的一段时间横着不动一段时间忽然抬升。这种“横盘 抬升”的形状就是空仓等待的真实效果没有信号时账户净值不波动有趋势时净值快速拉升。如何判断回测是否成功核心看三点代码能正常跑完没有报错。输出的指标里包含总收益率、最大回撤、空仓占比等。净值图能显示两条不同颜色的曲线策略净值有明显波动但形态不同于买入持有。如果运行失败第一步先看报错信息绝大多数问题出在 pandas 的索引对齐或 NaN 数据处理上。也可以把代码逐步拆开跑在generate_signal后先打印df.head()确认信号列是否正确。7. 常见问题与排查思路7.1 回测收益率很高但实盘老是亏钱这是最常见也最危险的问题。多数原因是回测里包含了未来函数或者没有计入手续费和滑点。问题现象可能原因排查方式解决方案策略回测收益明显高于实盘信号未做延迟处理使用了未来数据检查持仓信号是否使用了shift(1)对持仓信号统一做延迟一天处理实际成交价格与回测假设差异大未建模滑点和手续费查看每次交易的买入价和卖出价回测中加入手续费和滑点模型实盘交易次数过多策略参数过于敏感信号频繁翻转检查信号生成逻辑在不同参数下的稳定性适当放大均线周期或加入额外过滤条件同一套参数在不同时间段表现差距大过拟合或者市场风格切换做样本内外测试不要反复调参固定规则后测试长期表现7.2 信号翻转过于频繁双均线策略最常见的缺陷是在震荡行情中反复发出金叉死叉。如果交易次数太多手续费会侵蚀利润。改进方向引入更长的均线周期比如MA30/MA90降低信号频率。加入过滤条件比如要求短期均线与长期均线的差值超过某个阈值才触发信号。改用最高价与最低价计算的通道突破类指标比如唐奇安通道。7.3 空仓时间太长错过了部分上涨这是趋势跟踪策略的固有代价。你不可能既在震荡期完全空仓又能在每段上涨启动的第一天满仓。判断策略好坏要看整体期望不是看某一次“踏空”。正确的心理模型是用震荡期的小亏或空仓换取趋势期的较大盈利。在长期统计中只要趋势行情的盈利能够覆盖震荡期的损耗策略就是正期望。7.4 数据里有停牌或缺失值回测时遇到缺失值不要直接删除可能破坏时间连续性。推荐先用前向填充df df.fillna(methodffill)如果停牌时间过长建议直接在数据清洗阶段剔除或者用复权后的数据重新计算。8. 最佳实践从回测到实盘的工程建议8.1 先跑通逻辑再优化参数很多新手一上来就在调均线参数20 日和 60 日不行就换 10 日和 30 日换来换去最后陷入过拟合。正确的顺序是先用默认参数跑通整体流程确认每个环节的逻辑都没有问题再考虑参数优化。优化参数时必须做样本外验证。把历史数据分为前 80% 和后 20%在前 80% 上选参数在后 20% 上做验证。如果后 20% 的表现远差于前 80%说明参数过拟合需要谨慎对待。8.2 交易成本必须计入回测实盘交易的成本至少包括佣金、印花税、滑点。对于日线级别的策略单次交易成本可以按双边千分之一到千分之二估算。在向量化回测中加入交易成本并不复杂在每次仓位变化时扣减一笔固定成本即可。这里给出一个最小实现思路# 简单交易成本示例每次换仓扣除 0.1% 成本 trade_cost 0.001 position_changed df[position_shift].diff().abs().fillna(0) df[strategy_return] df[strategy_return] - position_changed * trade_cost8.3 策略日志是实盘最重要的工程设施回测阶段你可能只关心最终收益曲线。但到了实盘你必须知道每一次信号触发的原因、当时的市场状态、参数值、成交价格。建议每次信号产生时输出结构化日志字段包括时间戳信号类型买入/卖出/空仓策略参数均线周期、当前价格触发原因金叉/死叉/风控离场有了日志策略出错时你才能定位问题而不是对着净值曲线猜。8.4 风控优先于收益对散户来说最大回撤比年化收益率更值得关注。一个年化 30% 但最大回撤 40% 的策略实盘里绝大多数人拿不住。在设计策略时可以在信号层之上再加一层风控规则单笔亏损超过一定比例强制离场。累计回撤超过阈值降低仓位或暂停交易。只在交易时段内允许开仓收盘前检查持仓状态。这些规则不会提高策略收益但它们决定了你能不能活着等到策略的“有效区间”。8.5 不要忽略系统设计如果策略将来要运行在服务器上还需要考虑定时获取数据并计算信号。信号变化时通过消息推送提醒。交易日历管理避免在非交易日产生操作。数据库存储历史信号和成交记录。这些内容已经超出了简单回测的范畴但会让你的策略从“脚本”变成“系统”。9. 总结这篇文章的核心信息可以浓缩成三句话散户亏损主因往往不是分析能力差而是缺乏空仓纪律频繁操作在震荡市中反复磨损本金。空仓等待不应该是被动结果而应该通过信号系统主动实现双均线趋势跟踪就是一个简洁有效的入门方案。回测不是写一个脚本看收益曲线就结束了信号延迟、交易成本、样本外验证和风控规则才是从回测走向实盘的关键工程环节。建议你按本文代码先跑通一版完整回测然后做三件事把模拟数据替换成真实历史行情数据重新回测观察指标变化。将双均线策略换成其他趋势跟踪指标比如唐奇安通道突破、MACD 趋势确认对比不同策略的“空仓时间占比”。在回测中加入手续费和滑点模型看看策略收益被吃掉多少。做得越多你会越明白交易系统的核心竞争力不在于信号的“神准”而在于规则的一致性、风控的严格性和面对震荡时的耐心。80% 的时间空仓等待听起来很无聊但真正执行下来已经超过了大多数“管不住手”的散户。