思考题:金叉一定赚钱吗?
不一定。 金叉只是表示短期均线上穿长期均线,反映的是已经发生的价格变化,而不是未来预测 。在震荡行情中,金叉和死叉会频繁交替出现,导致反复止损。
真正验证策略是否赚钱,需要靠 回测 ------通过回测,可以计算出策略的:总收益率、胜率(赚钱交易的占比)、最大回撤(最坏情况下的亏损幅度)。所以:金叉是信号,回测才是答案。
量化策略的第一步,就是先把 大方向(趋势) 和 每日乱动(噪声) 分开

三个状态下举个栗子:
bash
# ========== 模拟三种市场状态并分区上色 ==========
np.random.seed(7) # 固定随机数,图可复现
n_up, n_down, n_noise = 60, 50, 70 # 上涨段、下跌段、横盘段各多少天
ret_up = np.random.normal(0.004, 0.008, n_up) # 上涨段:平均日收益偏正
ret_down = np.random.normal(-0.005, 0.010, n_down) # 下跌段:平均日收益偏负
ret_noise = np.random.normal(0.0, 0.015, n_noise) # 横盘:均值约0,波动大
price = 100 * np.cumprod(1 + np.r_[ret_up, ret_down, ret_noise]) # 拼成价格
x = np.arange(len(price)) # 横轴:第几个交易日
fig, ax = plt.subplots(figsize=(13, 5))
ax.plot(x, price, color='black', linewidth=1.2, label='价格')
ax.axvspan(0, n_up, alpha=0.15, color='green', label='上涨趋势')
ax.axvspan(n_up, n_up + n_down, alpha=0.15, color='red', label='下跌趋势')
ax.axvspan(n_up + n_down, len(price), alpha=0.15, color='gray', label='噪声/横盘')
ax.set_title('三种市场状态(模拟):趋势 vs 噪声', fontsize=14)
ax.set_xlabel('交易日(示意)')
ax.set_ylabel('价格')
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print('绿色区:整体向上 | 红色区:整体向下 | 灰色区:方向不明显、抖动大')

为什么需要「平均」?
真实股票 每天波动太乱:把最近几天的价格取平均,留下更平滑的「趋势线」。
bash
# ========== 对比「乱的价格」和「平滑的均线」==========
demo = pd.DataFrame({'Close': price}) # 用上面模拟的价格
demo['MA20'] = demo['Close'].rolling(20).mean() # 20日移动平均
fig, ax = plt.subplots(figsize=(13, 5))
ax.plot(demo['Close'], label='原始收盘价(很乱)', color='lightgray', linewidth=1.5)
ax.plot(demo['MA20'], label='20日移动平均线(更平滑)', color='tab:blue', linewidth=2)
ax.set_title('为什么需要平均?------ 磨平噪声,看清趋势', fontsize=14)
ax.set_xlabel('交易日(示意)')
ax.set_ylabel('价格')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

n 日简单移动平均线 的定义:最近 n 天收盘价的算术平均。
你可以把价格想象成你每天早上的体重:
-
什么是 MA5(5日均线)?------ 只关心"近5天"的平均体重
你每天早上称一次体重。MA5 就是把最近5天的体重加起来除以5。
如果你昨天吃多了,今天重了 1 斤,这个"1斤"立刻就会被算进 MA5 里。
因为只算了5天,只要今天有一点点变化,MA5就会跟着快速地上下跳动。
👉 结论:MA5 很敏感,价格一涨它就涨,价格一跌它就跌,跟得很紧。
-
什么是 MA20(20日均线)?------ 关心"近20天"的平均体重
MA20 是把最近20天的体重加起来除以20。
你今天重了 1 斤,但因为过去 20 天里有很多天的老数据在里面"垫着",这个 1 斤对平均值的影响被稀释了。
哪怕你连续重了三天,MA20 可能只是微微往上翘,不会一惊一乍。
👉 结论:MA20 很迟钝,价格短期上蹿下跳,它都"懒得动",非常平滑。
bash
# ========== 下载真实股票并计算 MA5、MA20 ==========
raw = yf.download(TICKER, period=PERIOD, progress=False, multi_level_index=False) # 下载行情
df = raw[['Close']].dropna().copy() # 只留收盘价,去掉空行
df.columns = ['Close'] # 列名统一成 Close
df['MA5'] = df['Close'].rolling(5).mean() # 5日均线 = 最近5天收盘均价
df['MA20'] = df['Close'].rolling(20).mean() # 20日均线
print(f'{TICKER} 共 {len(df)} 个交易日')
display(df.tail(8)) # 显示最后8行,检查算得对不对
# ========== 画收盘价 + 两条均线 ==========
plt.figure(figsize=(13, 5))
plt.plot(df.index, df['Close'], label='收盘价', color='gray', alpha=0.5, linewidth=1)
plt.plot(df.index, df['MA5'], label='MA5(5日均线)', color='tab:orange', linewidth=1.5)
plt.plot(df.index, df['MA20'], label='MA20(20日均线)', color='tab:blue', linewidth=2)
plt.title(f'{TICKER}:价格与移动平均线', fontsize=14)
plt.xlabel('日期')
plt.ylabel('价格')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

当 短期均线 和 长期均线 放在一起,会出现两种经典形态:
1. 先理解两条线的"角色"
在讲交叉之前,你必须先记住一句话:
MA5(短期均线) = 反应快的"年轻人",价格一变它就跟上。
MA20(长期均线) = 反应慢的"老人",价格变好几天,它才慢慢反应过来。
2. 金叉------ 年轻人从下往上穿过了老人
想象这样一个场景:
一个体力好的年轻人(MA5)和一位沉稳的老人(MA20)在赛跑。
刚开始,老人走在前面(MA20在上面,MA5在下面,价格处于下跌趋势)。
突然,年轻人开始加速,他从老人身后追了上来,从下方穿到了上方(MA5 上穿 MA20)。
这就叫 金叉 。
短期价格突然变强了,强到把长期均线都踩在脚下了。这说明涨的力量开始占上风,所以大家把它看作 "偏多 / 买入参考"。
3. 死叉------ 年轻人从上往下掉到了老人下面
反过来看:
之前年轻人领跑(MA5在上面,MA20在下面,价格处于上涨趋势)。
现在年轻人跑不动了,速度慢下来,老人依然不紧不慢地走着。
年轻人从上方掉下来,穿到了老人的下方(MA5 下穿 MA20)。
这就叫 死叉 。
短期价格突然变弱了,连慢吞吞的长期均线都守不住。这说明跌的力量开始占上风,所以大家把它看作 "偏空 / 卖出参考"。
金叉 = 短期力量强于长期力量 → 市场在变强
死叉 = 短期力量弱于长期力量 → 市场在变弱
金叉(MA5 上穿 MA20)≈ 买入参考;死叉 ≈ 卖出参考。
bash
# ========== 检测金叉、死叉 ==========
df['spread'] = df['MA5'] - df['MA20'] # 短均线减长均线
df['cross'] = np.sign(df['spread']).diff() # 符号变化:正=金叉,负=死叉
golden = df[df['cross'] > 0].dropna(subset=['MA5', 'MA20']) # 金叉那些天
death = df[df['cross'] < 0].dropna(subset=['MA5', 'MA20']) # 死叉那些天
print(f'样本期内 金叉 {len(golden)} 次,死叉 {len(death)} 次')
print('\n最近 3 次金叉日期:')
print(golden.tail(3).index.strftime('%Y-%m-%d').tolist())
print('\n最近 3 次死叉日期:')
print(death.tail(3).index.strftime('%Y-%m-%d').tolist())

bash
# ========== 金叉死叉标注图 ==========
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(df.index, df['Close'], color='gray', alpha=0.4, linewidth=1, label='收盘价')
ax.plot(df.index, df['MA5'], color='tab:orange', linewidth=1.5, label='MA5')
ax.plot(df.index, df['MA20'], color='tab:blue', linewidth=2, label='MA20')
ax.fill_between(df.index, df['MA5'], df['MA20'],
where=(df['MA5'] >= df['MA20']),
interpolate=True, alpha=0.12, color='green', label='MA5 > MA20')
ax.scatter(golden.index, golden['MA5'], marker='^', s=80, color='green',
edgecolors='black', linewidths=0.5, zorder=5, label='金叉(买入参考)')
ax.scatter(death.index, death['MA5'], marker='v', s=80, color='red',
edgecolors='black', linewidths=0.5, zorder=5, label='死叉(卖出参考)')
ax.set_title(f'{TICKER}:MA5 vs MA20 ------ 金叉 ▲ 与 死叉 ▼', fontsize=14)
ax.set_xlabel('日期')
ax.set_ylabel('价格')
ax.legend(loc='upper left', fontsize=9)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

了解到这里就可以开始第一个策略了
bash
如果 MA5 > MA20:
持有 / 买入
否则:
空仓 / 卖出
在 pandas 里,signal = 1 表示持仓,0 表示空仓:
bash
# ========== 第一个策略:MA5>MA20 则持仓 ==========
df['signal'] = (df['MA5'] > df['MA20']).astype(int) # 满足条件=1,否则=0
df['trade'] = 0 # 默认无交易
df.loc[df['cross'] > 0, 'trade'] = 1 # 金叉日标记买入
df.loc[df['cross'] < 0, 'trade'] = -1 # 死叉日标记卖出
hold_days = df['signal'].sum() # signal=1 的天数
print(f'规则:MA5 > MA20 则持仓 (signal=1)')
print(f'样本期内约 {hold_days} 个交易日处于持仓状态(共 {len(df)} 天)')
print(f'共产生 { (df["trade"] != 0).sum() } 次调仓信号(买+卖)')
display(df[df['trade'] != 0][['Close', 'MA5', 'MA20', 'signal', 'trade']].tail(6))
# 真实交易还要考虑手续费、滑点、能否当天成交等;第四章回测 会帮你用数据检验「这套规则到底赚没赚」。
# 小提示:为避免 未来函数,回测时常用 signal.shift(1)------今天收盘算出的信号,明天才能按它交易。本章先聚焦「规则与可视化」。

可视化策略信号(非常重要)
bash
# 价格 + 两条均线
# 绿色 ▲ = 买入点(金叉)
# 红色 ▼ = 卖出点(死叉)
# 下方色带 = 当前是持仓还是空仓
# ========== 策略信号大图:价格+买卖点+持仓条 ==========
buys = df[df['trade'] == 1] # 所有买入日
sells = df[df['trade'] == -1] # 所有卖出日
fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True,
gridspec_kw={'height_ratios': [3, 1]})
ax_price, ax_pos = axes
ax_price.plot(df.index, df['Close'], color='gray', alpha=0.45, linewidth=1, label='收盘价')
ax_price.plot(df.index, df['MA5'], color='tab:orange', linewidth=1.5, label='MA5')
ax_price.plot(df.index, df['MA20'], color='tab:blue', linewidth=2, label='MA20')
ax_price.scatter(buys.index, buys['Close'], marker='^', s=120, color='limegreen',
edgecolors='darkgreen', linewidths=1, zorder=6, label='买入 ▲')
ax_price.scatter(sells.index, sells['Close'], marker='v', s=120, color='salmon',
edgecolors='darkred', linewidths=1, zorder=6, label='卖出 ▼')
ax_price.set_title(f'{TICKER} 双均线策略:均线 + 买卖点', fontsize=14)
ax_price.set_ylabel('价格')
ax_price.legend(loc='upper left')
ax_price.grid(True, alpha=0.3)
ax_pos.fill_between(df.index, 0, df['signal'], step='post', alpha=0.35, color='steelblue')
ax_pos.set_ylim(-0.1, 1.2)
ax_pos.set_yticks([0, 1])
ax_pos.set_yticklabels(['空仓 (0)', '持仓 (1)'])
ax_pos.set_xlabel('日期')
ax_pos.set_ylabel('信号')
ax_pos.set_title('策略持仓状态:MA5 > MA20 时持有', fontsize=12)
ax_pos.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

bash
# ========== 放大最近6个月,看清买卖细节 ==========
recent = df.last('6M') if len(df) > 120 else df.tail(120) # 取最近约6个月
buys_r = recent[recent['trade'] == 1]
sells_r = recent[recent['trade'] == -1]
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(recent.index, recent['Close'], color='gray', alpha=0.5, linewidth=1, label='收盘价')
ax.plot(recent.index, recent['MA5'], color='tab:orange', linewidth=1.8, label='MA5')
ax.plot(recent.index, recent['MA20'], color='tab:blue', linewidth=2.2, label='MA20')
ax.scatter(buys_r.index, buys_r['Close'], marker='^', s=140, color='limegreen',
edgecolors='darkgreen', linewidths=1, zorder=6, label='买入')
ax.scatter(sells_r.index, sells_r['Close'], marker='v', s=140, color='salmon',
edgecolors='darkred', linewidths=1, zorder=6, label='卖出')
ax.set_title(f'{TICKER} 近 6 个月:金叉买入 / 死叉卖出(局部放大)', fontsize=14)
ax.set_xlabel('日期')
ax.set_ylabel('价格')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
