作 者:老余捞鱼
原创不易,转载请标明出处及原作者。

写在前面的话:今天我分享一个ETF调仓套利策略。它利用月末机构调仓的规律,在TLT和SPY间做对冲交易,追求稳定的小收益。代码简单,我手把手教你回测和优化,适合新手入门。不用复杂模型,就能捕捉市场机会!
一、月末调仓套利策略揭秘
很多人觉得,想在股市里赚钱,就得搞点高深莫测的东西。什么AI预测、高频交易、深度学习……听着就头大。
其实啊,真正的“印钞机”策略,往往藏在最不起眼的地方。
今天我要分享的,就是一个“傻瓜式”策略——每个月只动两次手,就能从市场里“薅羊毛”。
它不靠猜明天是涨是跌,也不靠听专家喊“牛市来了”。它靠的是——基金公司月底“装门面”的老毛病。
你没听错,就是那些管理着几百亿、几千亿资金的大佬们,每到月底、季末,都会偷偷调整一下自己的持仓,让报表看起来更漂亮。
比如,卖掉表现差的股票,多买点涨得好的;或者,为了显得“稳健”,临时买点国债。
这一买一卖,就会让市场在特定时间点出现“小波动”。而我们,就是要抓住这些“小波动”,稳稳地赚点小钱。
这个策略的核心,就是两只ETF:
- TLT:跟踪美国长期国债,相当于“避风港”,股市跌的时候它常涨。
- SPY:跟踪标普500指数,代表美国大盘股,股市涨它就涨。
这两兄弟,平时走势经常“对着干”,简直是天生一对。
我们的操作很简单:
每月倒数第7个交易日:买TLT,卖空SPY
下个月第一个交易日:卖TLT,买回SPY
再过一周:全部平仓
就这么简单。不用盯盘,不用分析财报,不用研究宏观经济。只要记住两个日子,按计划操作就行。
这叫“市场中性策略”——不赌大盘涨跌,只赚两只ETF之间的“相对差价”。
好处是啥?风险低啊!就算2022年那种熊市,股市和债市一起跌,但它们跌的速度不一样,我们照样能赚到“剪刀差”。
二、为什么这个策略能“偷懒赚钱”?
说白了,这就是在“薅机构的羊毛”。
基金公司年底或季末要发报告,老板和客户都要看。他们不想看到自己重仓了一堆“垃圾股”,所以会做点“美化”。
比如:
- 卖掉最近表现差的股票,换成涨得好的。
- 临时买点国债,显得“风控做得好”。
这种行为,在金融圈有个专有名词——“窗口装饰”(Window Dressing)。
听起来很专业,其实就是“临考前突击背书”。
历史上就有经典案例:
- 2008年雷曼兄弟:季度末用“Repo 105”把负债暂时移出报表,假装自己没那么危险。
- 普通公募基金:季末狂卖亏损股,猛买明星股,导致股价被“扭曲”几个百分点。
- 保险公司:月底集中买国债,推高价格,导致国债最后几天平均能涨0.25%。
这些行为,都是有规律可循的。而我们的策略,就是专门抓这些“规律”。
这不是赌博,这是利用人性弱点赚钱。
三、手把手教学
下面,我手把手带你用Python把这个策略跑起来。全程代码清晰,注释详细,照着敲就能跑。
第一步:安装必备工具包
打开你的Python环境(推荐Jupyter Notebook或Google Colab),先装这几个库:
pip install yfinance pandas numpy vectorbt如果你不会装,直接去Google Colab,新建一个Notebook,粘贴代码就能跑,完全免费!
第二步:导入数据(2010年至今)
这段代码从2010年到现在下载TLT和SPY的数据。yfinance库能自动从雅虎财经抓数据,超级方便。
import yfinance as yf
import pandas as pd
import numpy as np
import vectorbt as vbt
# 设置时间范围
start_date = "2010-01-01"
end_date = pd.Timestamp.today().strftime("%Y-%m-%d") # 自动获取当前日期
symbols = ["TLT", "SPY"] # 我们要操作的两只ETF
# 下载收盘价
try:
price_data = yf.download(symbols, start=start_date, end=end_date, auto_adjust=False)["Close"]
except Exception as e:
print(f"下载数据失败: {e}")
# 可以换Alpha Vantage等其他数据源
# 填充缺失值(比如节假日)
price_data = price_data.fillna(method='ffill').dropna()第三步:找关键交易日
我们要找的是:
- 每个月倒数第7个交易日(开仓日)
- 每个月第一个交易日(平仓+反向开仓日)
- 平仓日之后第7个交易日(最终平仓日)
代码如下:
dates = price_data.index
year_month = dates.to_period('M')
grouped = pd.DataFrame({"dt": dates, "ym": year_month}).groupby("ym")["dt"]
first_trading_days = grouped.first().values # 每月第一个交易日
last_trading_days = grouped.last().values # 每月最后一个交易日
# 定义函数:找前面第N个交易日
def get_prev_trading_day_idx(trading_dates, base_dates, offset):
idx = []
trading_dates = pd.Series(trading_dates)
for d in base_dates:
pos = trading_dates.searchsorted(d) # 找到插入点
prev_idx = pos - offset
if prev_idx >= 0:
idx.append(trading_dates.iloc[prev_idx])
return pd.DatetimeIndex(idx)
# 定义函数:找后面第N个交易日
def get_offset_trading_day_idx(trading_dates, base_dates, offset):
idx = []
trading_dates = pd.Series(trading_dates)
for d in base_dates:
pos = trading_dates.searchsorted(d)
target_idx = pos + offset
if target_idx < len(trading_dates):
idx.append(trading_dates.iloc[target_idx])
return pd.DatetimeIndex(idx)
# 计算关键日期
pre_end_idx = get_prev_trading_day_idx(dates, last_trading_days, 7) # 7天前开仓
month_start_idx = get_offset_trading_day_idx(dates, last_trading_days, 1) # 月初反向
week_after_start_idx = get_offset_trading_day_idx(dates, month_start_idx, 7) # 一周后平仓第四步:创建交易信号
我们创建一个信号表,告诉程序什么时候买、什么时候卖。
# 初始化信号表
signals = pd.DataFrame(
index=dates,
columns=pd.MultiIndex.from_product([symbols, ['long_entry', 'long_exit', 'short_entry', 'short_exit']]),
data=False
)
# 开仓信号:月底前7天,买TLT,卖SPY
signals.loc[pre_end_idx, ("TLT", "long_entry")] = True
signals.loc[pre_end_idx, ("SPY", "short_entry")] = True
# 反向开仓信号:月初,卖TLT,买SPY
signals.loc[month_start_idx, ("TLT", "short_entry")] = True
signals.loc[month_start_idx, ("SPY", "long_entry")] = True
# 平仓信号:月初平掉第一笔,一周后平掉第二笔
signals.loc[month_start_idx, ("TLT", "long_exit")] = True
signals.loc[month_start_idx, ("SPY", "short_exit")] = True
signals.loc[week_after_start_idx, ("TLT", "short_exit")] = True
signals.loc[week_after_start_idx, ("SPY", "long_exit")] = True
# 整理成vectorbt能识别的格式
long_entry = pd.DataFrame({sym: signals[(sym, "long_entry")] for sym in symbols})
long_exit = pd.DataFrame({sym: signals[(sym, "long_exit")] for sym in symbols})
short_entry = pd.DataFrame({sym: signals[(sym, "short_entry")] for sym in symbols})
short_exit = pd.DataFrame({sym: signals[(sym, "short_exit")] for sym in symbols})这些函数帮你精准定位交易点,避免错过时机。
第五步:回测策略
最后,我们用vectorbt回测,看看策略表现。假设起始资金10万美元。代码如下:
# 开始回测,初始资金10万美金
pf = vbt.Portfolio.from_signals(
price_data,
entries=long_entry,
exits=long_exit,
short_entries=short_entry,
short_exits=short_exit,
freq='D',
size_type=1, # 用百分比下单
size=np.inf, # 无限资金(简化版,实际可用size=1)
init_cash=100_000
)
# 输出统计结果
print(pf.stats())运行完,你会看到类似这样的结果:

是不是看着不高?别急,这可是15年无脑操作的结果,而且最大回撤只有9%,风险极低。
对比一下,同期买入持有SPY,年化可能有7-8%,但最大回撤能到30%以上。
我们这个策略,胜在稳。
四、进阶玩法
上面的代码是基础版,如果你想让它更赚钱,可以试试这些改进:
1. 调整开仓时间
原版是“月底前7天”,你可以改成“前3天”或“前10天”,看看哪个效果更好。
pre_end_idx = get_prev_trading_day_idx(dates, last_trading_days, 3) # 改成3天前2. 换ETF组合
除了TLT和SPY,还可以试试:
- IEF(中期国债) + SPY
- QQQ(纳斯达克) + TLT
- AGG(综合债券) + QQQ
不同的组合,有不同的“剪刀差”。
3. 加个“趋势过滤器”
如果最近一个月,TLT涨得比SPY多,我们就执行策略;否则,休息。
# 计算20日涨幅
tlr_change = price_data['TLT'].pct_change(20)
spy_change = price_data['SPY'].pct_change(20)
# 只有当TLT涨得更多时,才开仓
trade_filter = tlr_change > spy_change
# 应用到开仓信号
long_entry_filtered = long_entry & trade_filter
short_entry_filtered = short_entry & trade_filter这样能避免在“极端行情”下亏钱。
4. 加入手续费和融券成本
现实中,卖空SPY是要付利息的,大概每年0.3%-1%。我们可以模拟一下:
pf = vbt.Portfolio.from_signals(
price_data,
entries=long_entry,
exits=long_exit,
short_entries=short_entry,
short_exits=short_exit,
freq='D',
size_type=1,
size=np.inf,
init_cash=100_000,
fees=0.001, # 每笔交易0.1%手续费
short_fees=0.0005 # 卖空费,按日计算(约0.5%/年)
)加上成本后,收益会略降,但更贴近现实。
五、观点总结
总的来说,这是一个“低风险、低收益、高稳定”的策略。它不适合追求暴富的人,但非常适合:
- 想学量化交易的新手。
- 不想天天盯盘的上班族。
- 希望资产稳健增值的保守派。
它最大的优点是:简单、透明、可验证。你不需要相信任何“大师”,只需要相信数据和代码。
- 每月只操作两次,省时省力,适合懒人。
- 利用基金“窗口装饰”行为,赚取市场“小波动”。
- 代码开源,小白照抄就能跑,附带详细注释。
- 风险极低,最大回撤不到10%,适合保守投资者。
- 可优化空间大,换ETF、调时间、加过滤器,都能提升收益。
#关键词
#Python量化 #ETF交易 #基金调仓 #月度策略 #躺赚策略 #投资理财 #量化交易入门 #Python代码 #金融干货 #A股美股 #被动收入 #投资小白 #回测教程 #市场中性 #低风险高收益 #低风险投资 #对冲套利
本文代码我已经尽量写得简单易懂,大家可以直接复制使用。如果对文中内容有任何疑问,欢迎留言,我会尽快回复。祝您投资顺利,收益长虹!
本文内容仅限技术探讨和学习,不构成任何投资建议。
Be First to Comment