01 · 因子与信号¶
⚠️ 免责声明:本研究为量化/短期交易方法与可行性的探讨。所有回测均为历史模拟,不代表未来收益,不构成任何投资建议。真实交易受流动性、冲击成本、税费、心理与执行误差影响,结果通常更差。量化与短期交易可致重大本金亏损。读者需自行承担一切决策后果。
数据来源:A股 akshare(新浪后端) / 美股 yfinance,限流时降级腾讯行情;全部缓存于
data/*.csv,每个数字均可在本 notebook 单元复现。
构造短期/量化里最常见的四类信号,并可视化:
- 趋势——双均线交叉
- 动量——过去N月收益
- 均值回归——RSI / 布林带
- 横截面多因子打分
本 notebook 只做信号构造与解读,不下回测结论(回测在 02)。关键纪律:信号都用「截至当日收盘」的信息计算,绝不引用未来数据。
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import qr_data as q, qr_bt as bt
bt.setup_chinese_font(); np.random.seed(42)
data=q.load_all(verbose=False)
print('已载入', len(data), '个标的')
已载入 10 个标的
1. 趋势:双均线交叉¶
最古老的趋势跟踪。快线(20日)上穿慢线(60日)视为上升趋势→持有;下穿→空仓。
px=data['SPY']['close']
fast,slow=20,60
f=px.rolling(fast).mean(); s=px.rolling(slow).mean()
sig=bt.sma_cross_signal(px,fast,slow)
fig,ax=plt.subplots(figsize=(12,4.5))
ax.plot(px.index,px,lw=0.8,label='SPY 收盘',color='black')
ax.plot(f.index,f,lw=1,label=f'{fast}日均线')
ax.plot(s.index,s,lw=1,label=f'{slow}日均线')
ax.fill_between(px.index, px.min(), px.max(), where=sig>0, alpha=0.08, color='green', label='持有区间')
ax.set_title('SPY 双均线(20/60)信号'); ax.legend(fontsize=8); plt.tight_layout(); plt.show()
print('持有天数占比 %.1f%%' % (sig.mean()*100), '| 信号翻转次数', int(sig.diff().abs().sum()))
持有天数占比 71.4% | 信号翻转次数 49
解读:双均线在单边趋势里跟得住,但在震荡市频繁「上穿→下穿」打脸(信号翻转次数即换手来源)。绿色持有区间漏掉了部分上涨、也躲过部分下跌——它本质是用收益换回撤,是否划算要靠 02 含成本回测说话。
2. 动量:过去N月收益¶
时间序列动量:过去约6个月(120日,跳过最近20日避免短期反转)收益为正→持有。学术界(Jegadeesh-Titman, Moskowitz)记录过动量溢价,但对个人的可实现性是另一回事。
px=data['QQQ']['close']
past=px.shift(20)/px.shift(140)-1
sig=bt.momentum_signal(px,lookback=120,gap=20)
fig,(a1,a2)=plt.subplots(2,1,figsize=(12,6),sharex=True,height_ratios=[2,1])
a1.plot(px.index,px,color='black',lw=0.8); a1.set_title('QQQ 价格与动量信号')
a1.fill_between(px.index,px.min(),px.max(),where=sig>0,alpha=0.08,color='green')
a2.plot(past.index,past,color='purple',lw=0.8); a2.axhline(0,color='red',lw=0.8)
a2.set_title('过去6月收益(动量因子值)'); plt.tight_layout(); plt.show()
print('动量为正占比 %.1f%%'%(sig.mean()*100))
动量为正占比 77.1%
解读:动量因子值穿越0轴的瞬间就是换仓点。趋势行情里动量持有率高、贴合上涨;但拐点处永远滞后(信号靠历史收益,必然慢半拍),在V型反转中先卖在底、再追在腰。这是动量的结构性代价。
3. 均值回归:RSI 与布林带¶
与趋势相反的世界观:价格短期超跌会反弹。RSI<30 超卖买入、>70 超买离场;或价格触布林下轨买入、回中轨离场。均值回归在震荡市有效、在单边趋势里会「接飞刀」。
px=data['600519']['close']
r=bt.rsi(px,14); ma,up,lo=bt.bollinger(px,20,2)
win=slice('2021-01-01','2023-12-31')
fig,(a1,a2)=plt.subplots(2,1,figsize=(12,6.5),sharex=True,height_ratios=[2,1])
a1.plot(px[win].index,px[win],color='black',lw=1,label='茅台')
a1.plot(ma[win].index,ma[win],lw=0.9,label='布林中轨')
a1.plot(up[win].index,up[win],lw=0.8,ls='--',color='gray')
a1.plot(lo[win].index,lo[win],lw=0.8,ls='--',color='gray',label='布林上/下轨')
a1.legend(fontsize=8); a1.set_title('茅台 布林带(20,2) 2021-2023')
a2.plot(r[win].index,r[win],color='orange',lw=1); a2.axhline(70,color='red',ls='--',lw=0.8)
a2.axhline(30,color='green',ls='--',lw=0.8); a2.set_title('RSI(14)'); a2.set_ylim(0,100)
plt.tight_layout(); plt.show()
解读:2021-2023 茅台是单边下行。注意 RSI 多次跌破30(看似「超卖买点」),但价格继续阴跌——均值回归在下跌趋势里就是连续接飞刀。这张图是给「抄底」叙事的照妖镜:指标给的不是「会涨」,只是「跌得多」。两者不是一回事。
4. 横截面多因子打分¶
机构选股的简化版:对一篮子股票,每天按多个因子打分排名再合成。这里用三个朴素因子:
- 动量:过去120日收益(越高越好)
- 低波动:过去60日波动率(越低越好)
- 趋势:现价相对200日均线乖离(>0为多头排列)
合成分 = 三个因子的横截面 z-score 等权平均。展示最新一期排名。
a_keys=[k for k in q.A_UNIVERSE if k!='CSI300']
closes=pd.DataFrame({k:data[k]['close'] for k in a_keys}).dropna()
mom=closes.pct_change(120)
vol=closes.pct_change().rolling(60).std()
trend=closes/closes.rolling(200).mean()-1
def zrow(df): return (df.sub(df.mean(axis=1),axis=0)).div(df.std(axis=1).replace(0,np.nan),axis=0)
score=(zrow(mom)+zrow(-vol)+zrow(trend))/3
latest=pd.DataFrame({'动量z':zrow(mom).iloc[-1],'低波z':zrow(-vol).iloc[-1],
'趋势z':zrow(trend).iloc[-1],'合成分':score.iloc[-1]}).round(2)
latest['名称']=[q.A_UNIVERSE[k]['name'] for k in latest.index]
latest.sort_values('合成分',ascending=False)
| 动量z | 低波z | 趋势z | 合成分 | 名称 | |
|---|---|---|---|---|---|
| 600036 | 0.38 | 0.49 | 0.84 | 0.57 | 招商银行 |
| 601318 | 1.48 | -0.90 | 0.94 | 0.51 | 中国平安 |
| 000333 | -0.13 | 0.79 | 0.36 | 0.34 | 美的集团 |
| 600519 | -1.15 | 0.87 | -1.14 | -0.47 | 贵州茅台 |
| 000858 | -0.58 | -1.26 | -0.99 | -0.94 | 五粮液 |
fig,ax=plt.subplots(figsize=(11,4))
top=score.mean(axis=1) # 组合平均分随时间
for k in a_keys: ax.plot(score.index, score[k], lw=0.8, label=q.A_UNIVERSE[k]['name'])
ax.axhline(0,color='black',lw=0.6); ax.legend(fontsize=8,ncol=5)
ax.set_title('5只A股合成因子分时间序列(>0 相对强于同侪)'); plt.tight_layout(); plt.show()
解读:多因子打分把「绝对预测」降级为「相对排序」——它不保证赚钱,只回答「这5只里现在哪只相对更强」。5只股票的横截面太窄,z-score 噪声极大;真要做因子选股,标的池得是几百上千只(全市场),这就引出 02/03 要算的数据与算力门槛。
5. 小结¶
- 四类信号都能用十几行代码构造出来——写出信号从来不是难点。
- 趋势/动量滞后于拐点、均值回归会接飞刀、窄横截面打分噪声大:每个信号都有结构性代价。
- 信号「图上好看」毫无意义。下一步
02用含真实成本的回测,检验它们到底能不能赚到钱。