很多做交易的朋友都有这样一种体会:持仓比空仓难受,空仓比亏损难受。明明知道当前行情不好,却总想买点什么,生怕错过所谓的“大机会”;结果往往是买进去就被套,套住又舍不得止损,最后从小亏变成深亏。
这篇文章不聊技术指标公式的堆砌,也不搞“稳赚战法”那套玄学,而是从交易纪律和策略设计的角度,讲清楚一套“80%时间空仓等待”的保守型交易策略。它的核心思想很简单:大部分时间你应该什么都不做,只在少数满足条件的时间窗口出手。本文会包含完整的策略逻辑、可运行的代码示例、回测验证思路,以及常见问题的排查方法,适合做程序化交易的开发者,也适合想用规则约束自己手动交易的投资者参考。
1. 为什么“管不住手”是亏损的主要根源
1.1 过度交易的本质
在投资交易中,“过度交易”是散户亏损最普遍的原因之一。很多投资者把“每天都在操作”误当作“勤奋”,把“频繁买卖”误当作“抓住机会”。但实际上,市场的波动大部分是随机噪音,并不具备稳定的可交易性。
过度交易的本质,是不承认“市场大部分时间没有明确方向”这一事实。当我们频繁开仓时,每一笔交易都要承担手续费、滑点和错误的概率。即使单笔交易的胜率不低,一旦交易次数过多,交易成本和情绪损耗就会侵蚀利润。
更关键的是,频繁交易往往伴随情绪化决策。看到价格拉升就追进去,看到价格下跌就慌张割肉,这种操作方式根本没有一致的逻辑,结果自然也是随机的。
1.2 空仓等待并不是“没做事”
很多交易者觉得空仓等于浪费时间,其实这是一种误解。在交易系统里,空仓是一种主动的风险管理手段,意味着你正在等待系统给出高质量信号。
如果把交易看作打猎,空仓等待就是“瞄准再开枪”,而不是“开枪再瞄准”。真正的职业交易者往往有着极高的耐心,他们只在自己最有把握的行情下出手,其余时间都在观察和等待。
将“80%时间空仓”作为策略目标,并不是要求你一定空仓,而是提醒你:大部分时间市场的交易价值有限,你应该把精力和资金留给那些少数的、值得出手的机会。
1.3 策略化交易才是出路
“管不住手”是情绪层面的问题,但解决它不能只靠自我控制,要靠策略和规则。
如果你在交易前就定义好了入场条件、出场条件、仓位大小、止损位置,那么每一次操作都不是临时起意,而是按规则执行。这种“规则化”的交易方式,也就是我们常说的量化交易思想,即使你没有写代码,也可以手动执行。
本文介绍的“空仓等待策略”就是一套规则化的交易框架:它用趋势指标判断大方向,用波动指标过滤震荡,用时间条件规避不确定性,多个条件全部满足才考虑入场,否则就保持空仓。
2. 策略整体逻辑与设计思路
2.1 核心思想:弱水三千,只取一瓢
这套策略的设计目标不是捕捉所有上涨行情,而是过滤掉大部分无效行情,只参与趋势明确、风险相对可控的时间段。
用一句话概括:多条件共振才入场,条件不满足就空仓等待。
它包含三个核心模块:
- 方向过滤:判断市场当前处于上升趋势还是下降趋势。
- 强度过滤:判断当前趋势是否足够强,排除横盘震荡。
- 风险过滤:根据波动率调整仓位或设定止损,防止单笔亏损过大。
这三个模块分别对应“做什么方向”“现在能不能做”“做错了亏多少”三个问题。
2.2 为什么“多等一等”能提高胜率
很多交易策略追求“每次都参与”,结果在震荡行情中被反复止损。这套策略反过来,它故意降低交易频率,用“错过”来换取“少错”。
举个例子,如果一个策略每周发出5次信号,其中只有1次是有效的,那么它的胜率只有20%;如果过滤条件更严格,每周只发出1次信号,但这次信号的质量明显更高,那么胜率可能提升到50%以上。
当然,过滤条件越严格,错过的机会也越多。这是一个权衡。本文介绍的策略偏向保守,适合风险偏好较低、更看重资金回撤控制的投资者。
2.3 策略的适用场景
这套策略适用于以下场景:
- 日线或周线级别的中长线交易。
- 趋势特征明显的市场(如股票指数、主流币种、商品期货)。
- 有足够耐心等待信号出现、不追求每日交易的投资者。
它不适用于:
- 高频交易或日内超短线。
- 震荡明显的无趋势品种。
- 没有严格止损纪律、喜欢扛单的交易者。
3. 环境准备与版本说明
本策略的代码示例使用 Python 编写,依赖pandas、numpy和matplotlib库。版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
建议环境:
- Python 3.8 及以上版本。
- pandas 1.3 及以上版本。
- numpy 1.21 及以上版本。
- matplotlib 3.4 及以上版本。
如果你没有安装这些库,可以先执行以下命令:
pip install pandas numpy matplotlib如果你使用的是 Anaconda 环境,这些库通常已经内置,可以直接导入。
数据方面,本文以获取日线行情为例,使用市面上的常见数据接口。为了保护隐私和避免依赖特定 API,代码中预留了数据加载的位置,你可以替换为自己的数据源。
4. 核心指标与信号逻辑拆解
4.1 均线趋势过滤
均线是最常用的趋势判断工具之一。它的原理是计算一段时间内的平均价格,用平均价格的方向代表趋势方向。
在这套策略中,我们使用两条均线做趋势过滤:
- 50 日均线(中期趋势)
- 200 日均线(长期趋势)
信号逻辑:
- 当 50 日均线在 200 日均线上方时,认为市场处于多头趋势,只考虑买入。
- 当 50 日均线在 200 日均线下方时,认为市场处于空头趋势,禁止买入,保持空仓。
这个逻辑看起来简单,但非常有效。它保证了我们只在长期向上的市场中寻找机会,过滤掉了大部分下跌中继的反弹陷阱。
需要注意的是,均线本身具有滞后性。均线周期越长,反应越慢,但信号越稳定。短周期均线反应快,但容易被假突破欺骗。因此,本文没有使用过于灵敏的参数,而是选择稳健的中长周期。
4.2 ADX 趋势强度过滤
均线能判断方向,但无法区分“强趋势”和“缓慢爬升”。为了解决这个问题,我们引入 ADX(Average Directional Index,平均趋向指数)。
ADX 的取值范围是 0 到 100,数值越大代表趋势越强:
- ADX 大于 25,认为趋势较强,可以入场。
- ADX 小于 20,认为行情处于震荡或趋势不明朗,不宜入场。
- ADX 在 20 到 25 之间,属于临界区,建议观望。
ADX 的详细计算公式如下:
TR = max(high - low, abs(high - prev_close), abs(low - prev_close)) +DM = max(high - prev_high, 0) if (high - prev_high) > (prev_low - low) else 0 -DM = max(prev_low - low, 0) if (prev_low - low) > (high - prev_high) else 0然后对上述值做平滑处理,计算出 +DI 和 -DI,最后得到 DX,再对 DX 做平滑得到 ADX。
这个公式在代码中可以用 pandas 的滚动窗口直接实现,我们会在第 5 节给出完整代码。
4.3 ATR 波动率过滤与止损设定
ATR(Average True Range,平均真实波幅)用于衡量市场的波动程度。
ATR 的计算方式是对真实波幅(True Range)取 N 日平均值。真实波幅的计算公式如下:
TR = max(high - low, abs(high - prev_close), abs(low - prev_close))ATR 的用途有两个:
- 过滤波动率过低的行情,避免在死水一潭的品种中交易。
- 动态设定止损距离,根据波动率调整止损位。
在实际操作中,止损位可以设置为“入场价格 - 2 × ATR”。这意味着如果市场波动变大,止损距离会自动放宽;市场平稳时止损距离也会收窄。这种动态止损方式比固定百分比的止损更科学。
4.4 三个条件共振才入场
最终,策略的入场条件如下:
- 50 日均线位于 200 日均线上方。
- ADX 大于 25,表明趋势强度足够。
- 当前价格高于入场触发价,并且 ATR 保持在合理范围内。
三个条件同时满足时,生成买入信号;只要有一个条件不满足,就不开仓。
这个设计的好处是:单一指标可能会出现误判,但多个独立指标同时误判的概率会大幅降低。这就是我们在策略设计中常说的“多条件共振”。
5. 完整代码实现
5.1 项目结构
我们先创建项目目录,结构如下:
trade-strategy/ ├── data/ │ └── historical_data.csv ├── strategy.py └── README.md其中data/historical_data.csv存放历史行情数据,strategy.py是策略主文件。
建议数据至少包含以下列:date、open、high、low、close、volume,日期格式为YYYY-MM-DD。
5.2 策略核心代码
以下是完整的策略实现代码:
# 文件路径:strategy.py import pandas as pd import numpy as np import matplotlib.pyplot as plt # 读取数据 df = pd.read_csv('data/historical_data.csv', parse_dates=['date']) df.set_index('date', inplace=True) # 计算 50 日均线和 200 日均线 df['ma50'] = df['close'].rolling(window=50).mean() df['ma200'] = df['close'].rolling(window=200).mean() # 计算 TR(真实波幅) df['prev_close'] = df['close'].shift(1) df['tr'] = df.apply(lambda row: max( row['high'] - row['low'], abs(row['high'] - row['prev_close']), abs(row['low'] - row['prev_close']) ), axis=1) # 计算 ATR 14 df['atr'] = df['tr'].rolling(window=14).mean() # 计算 +DM 和 -DM df['up_move'] = df['high'].diff() df['down_move'] = -df['low'].diff() df['plus_dm'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0.0) df['minus_dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0.0) # 对 DM 做平滑处理 df['plus_dm_smooth'] = df['plus_dm'].rolling(window=14).mean() df['minus_dm_smooth'] = df['minus_dm'].rolling(window=14).mean() df['tr_smooth'] = df['tr'].rolling(window=14).mean() # 计算 +DI 和 -DI df['plus_di'] = 100 * df['plus_dm_smooth'] / df['tr_smooth'] df['minus_di'] = 100 * df['minus_dm_smooth'] / df['tr_smooth'] # 计算 DX 和 ADX df['dx'] = 100 * abs(df['plus_di'] - df['minus_di']) / (df['plus_di'] + df['minus_di']) df['adx'] = df['dx'].rolling(window=14).mean() # 策略信号:三个条件同时满足 df['trend_ok'] = df['ma50'] > df['ma200'] df['strength_ok'] = df['adx'] > 25 df['volatility_ok'] = df['atr'] > df['atr'].rolling(window=100).mean() * 0.8 df['signal'] = df['trend_ok'] & df['strength_ok'] & df['volatility_ok'] # 打印最近 10 条信号 print(df[['close', 'ma50', 'ma200', 'adx', 'atr', 'signal']].tail(10)) # 可视化 plt.figure(figsize=(12, 6)) plt.plot(df.index, df['close'], label='Close Price') plt.plot(df.index, df['ma50'], label='MA50') plt.plot(df.index, df['ma200'], label='MA200') plt.plot(df.index[df['signal']], df['close'][df['signal']], '^', markersize=10, color='r', label='Buy Signal') plt.legend() plt.title('Trend Following Strategy with ATR Filter') plt.show()5.3 代码说明
这段代码做的事情可以拆成几个部分:
- 读取数据后,用
rolling(window=50).mean()和rolling(window=200).mean()计算两条均线。 - 用
high - low、abs(high - prev_close)、abs(low - prev_close)三者中的最大值作为真实波幅 TR。 - 对 TR 取 14 日均值得到 ATR。
- 根据价格向上或向下的移动幅度判断正负方向摆动值 +DM、-DM。
- 对 DM 和 TR 做平滑后,计算 +DI、-DI,再得到 DX 和 ADX。
- 最终信号是趋势、强度、波动率三个布尔条件的与运算,只有全部为真时
signal列才为 True。
5.4 运行与验证
在项目目录下执行:
python strategy.py如果数据文件格式正确,你会看到终端输出最近 10 行数据的指标值,并弹出价格曲线图,图中红色三角标记代表符合入场条件的买点位置。
如果提示KeyError或FileNotFoundError,大概率是数据路径或列名与代码不一致,请先检查 CSV 文件。
6. 回测与结果评估
6.1 为什么必须回测
策略写完之后,不能直接上实盘。你需要先通过历史数据回测,验证策略在不同行情阶段的表现,这是量化交易的基本步骤。
回测的核心目标是回答几个问题:
- 策略的胜率是多少?
- 平均单笔盈利与平均单笔亏损的比值(盈亏比)是多少?
- 最大回撤是多少?是否能接受?
- 完整牛熊周期中,策略是否还能保持正收益?
如果策略只在牛市中赚钱、在熊市中大幅回撤,那么它并不是一个稳健的策略。
6.2 回测代码的基本框架
以下是一个简单的回测示例,它统计信号出现后的收益情况:
# 文件路径:backtest.py import pandas as pd import numpy as np # 假设 df 是包含 signal 列和 close 价格的数据框 # 这里用 df 变量作为演示 def backtest(df): # 取信号出现的日期 signal_dates = df.index[df['signal']].tolist() results = [] hold_days = 5 # 假设信号后持有 5 天 for date in signal_dates: try: idx = df.index.get_loc(date) entry_price = df['close'].iloc[idx] exit_price = df['close'].iloc[idx + hold_days] ret = (exit_price - entry_price) / entry_price results.append({ 'entry_date': date, 'exit_date': df.index[idx + hold_days], 'return': ret }) except IndexError: continue result_df = pd.DataFrame(results) if len(result_df) > 0: win_rate = (result_df['return'] > 0).mean() avg_return = result_df['return'].mean() max_drawdown = result_df['return'].min() # 使用累计收益计算最大回撤 cumsum = result_df['return'].cumsum() drawdown = cumsum - cumsum.cummax() max_dd = drawdown.min() else: win_rate = 0 avg_return = 0 max_dd = 0 print(f"信号数量: {len(result_df)}") print(f"胜率: {win_rate * 100:.2f}%") print(f"平均单次收益: {avg_return * 100:.2f}%") print(f"最大回撤: {max_dd * 100:.2f}%") # 调用回测 backtest(df)6.3 回测结果的解读
回测结果需要关注几个关键指标:
信号数量直接反映策略的“空仓比例”。如果信号数量很少,说明策略大部分时间确实在空仓。这个结果可能让人失望,因为交易频率低意味着“看似能赚钱的机会减少了”。但从风险管理角度看,过滤掉不确定性高的行情,才是长期复利的关键。
胜率是衡量策略是否有效的直接指标。稳健的策略胜率通常在 40% 到 60% 之间,结合合理的盈亏比,长期可以实现正期望。
最大回撤反映策略在极端行情下的亏损幅度。如果回撤超过你的心理承受范围,说明策略的仓位管理需要调整,或者过滤条件还可以更严格。
回测不是万能的,它基于历史数据,无法预测未来。但通过回测,你可以了解策略在不同市场状态下的行为特征,做到有的放矢。
7. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 策略几乎不发信号 | 过滤条件过于严格 | 检查 ADX 阈值,可适当降低到 22 或 20;检查均线周期是否过长 |
| 信号出现后经常小亏止损 | 入场后行情快速反转,止损距离太近 | 将止损从 1.5 倍 ATR 调整为 2 倍或 2.5 倍 ATR |
| 回测结果很好,实盘却不赚钱 | 数据前视偏差,或实际滑点手续费未计入 | 回测时预留手续费和滑点,避免过度拟合 |
| 历史数据加载报错 | CSV 列名或日期格式不匹配 | 检查数据的列名是否包含date、close等字段 |
| ADX 一直小于阈值 | 行情处于长期震荡期 | 该策略本身就不适用于震荡行情,建议切换品种或观望 |
| 信号在重大消息面前失效 | 突发事件跳空导致止损无效 | 增加仓位上限,避免在重大数据公布前持有头寸 |
这里重点解释一下“前视偏差”。前视偏差是指回测中意外使用了未来数据,导致回测结果虚高。比如,如果你在计算当日信号时意外引入了当日收盘后才会公布的数据,那么信号就会“提前知道答案”。避免前视偏差的方法是严格保证指标只使用当前时刻之前的数据,代码中所有rolling计算默认向后看,这是天然安全的,但如果你自定义了数据变换,需要格外注意。
8. 最佳实践与工程化建议
8.1 用规则替代意志力
“管不住手”的根源,是交易决策在盘中临时形成,容易受情绪影响。最佳实践是在盘前或盘中定时获取信号,一旦规则明确,就不再主观更改。
你可以做一个极简的交易清单:
- 今日 MA50 是否大于 MA200?
- 今日 ADX 是否大于 25?
- 今日是否出现策略入场信号?
只有所有答案都是“是”,才允许开仓。这个清单不需要复杂系统,一张纸就能执行。
8.2 仓位管理比入场信号更重要
即使信号质量很高,也不应该重仓押注。建议单笔风险控制在总资金的 1% 到 2% 以内,用 ATR 动态计算仓位大小,公式如下:
仓位 = (总资金 × 单笔风险比例) / (止损距离) 其中止损距离 = 2 × ATR举个例子,假设总资金 10 万元,单笔风险比例 1%,也就是你愿意承担 1000 元亏损,如果 2 倍 ATR 等于 200 点,那么你的仓位就是 1000 / 200 = 5 手。这个公式让风险始终可控,不会因为某次亏损而伤筋动骨。
8.3 日志记录是提升交易能力的捷径
每次交易后,记录以下信息:
- 入场理由、出场理由。
- 信号指标的具体数值。
- 是否按计划执行。
- 计划外操作的原因。
长期积累下来,你会发现自己的交易模式中哪些步骤容易出错,哪些环节需要重点改进。很多交易者亏损不是因为策略不好,而是因为没有记录,所以同样的错误反复犯。
8.4 先模拟、后实盘、再优化
新策略上线前,至少先做以下几个步骤:
- 用历史数据回测,确认策略逻辑没有编码错误。
- 用模拟盘运行 1 到 3 个月,观察实盘环境下的信号质量。
- 用最小仓位实盘验证,确认交易执行效率。
- 再逐步增加仓位,但每次调整必须记录结果,避免盲目优化。
策略优化要特别注意“过度拟合”的问题。如果你把参数调得完美贴合历史数据,那它很可能在未来失效。正确的做法是让策略逻辑尽量简洁,用少量的参数表达稳定的市场规律。
8.5 风险控制的底线思维
无论策略多好,都可能有连续亏损的阶段。这是交易的一部分,必须接受。
在资金管理上,建议设置“熔断机制”:当账户回撤达到 15% 时,暂停交易一周,复盘问题后再决定是否继续。这个机制不是策略层面的,而是风控层面的,它能防止你在连续亏损时因为情绪失控而做出更糟糕的决策。
9. 总结
“80%时间空仓等待”这类策略的核心,不是寻找更多的交易机会,而是学会过滤掉低质量的交易机会。它要求交易者承认自己不可能抓住所有行情,也要求交易者建立一套可验证、可执行的规则体系来约束自己。
从技术实现上看,趋势均线过滤方向、ADX 过滤强度、ATR 控制风险,这三个指标组合成一套完整的多条件共振策略。代码实现并不复杂,关键在于你是否愿意严格执行规则,是否能在信号不出现时耐心等待。
如果你还在亏损中反复循环,不妨先不做交易,把本文的策略代码跑一遍,用历史数据验证你的思路是否可行。用规则代替主观判断,用空仓代替频繁操作,长期来看,这才是普通投资者更稳妥的进步路径。
如果本文对你有帮助,可以先收藏备用,后续也可以多回测几组数据,看看不同参数下策略表现如何。祝每一次出手,都是计划内的一击。