
刚接触加密货币那会儿,看K线图的感觉就像在破解甲骨文——价格上蹿下跳,脑子里的声音喊"抄底!",然后市场一个反手给我砸下去10%。圈子里天天有人念叨"移动平均线"、"RSI",听着像是什么武功秘籍,但自己真上手的时候完全不知道从哪下爪。我的目标很简单:干掉那些假信号,不再被盘面的每一个小波动牵着鼻子走。我需要一套简单、可复现的方法来判断趋势是不是真的在启动,而不是纯粹的噪音。
说白了:移动平均线不是用来预测未来的,它是用来过滤噪音的——让你看清价格背后真正的趋势。简单移动平均线(SMA)就是一个滑动窗口,把最近n个价格做个平均。当价格上穿这条线,往往意味着多头在发力;下穿的话,道理相反。
RSI(相对强弱指数)是另一套逻辑。它衡量的是价格变动的速度和幅度,数值范围在0到100之间。超过70叫"超买"(可能该考虑卖出了),低于30叫"超卖"(可能是个买入机会)。真正有意思的是把这两个指标组合起来用:价格上穿SMA,同时RSI从超卖区爬出来,这个买入信号的强度比单独看任何一个都要靠谱得多。
我花了三个晚上跟Excel公式较劲,最后用几行Python代码搞定了。当图表上那些买卖标记跟实际反转点对得上的时候,那种感觉就像小时候发现了FC游戏里的隐藏关卡——纯粹的、毫无杂质的兴奋。
一开始我直接用显式循环来算,逻辑能跑通,但速度慢得要命,而且一不小心就出错。
# 🚫 用循环算 SMA 和 RSI(别学我)
def sma_loop(prices, window): sma = [] for i in range(len(prices)): if i < window - 1: sma.append(None) else: window_slice = prices[i - window + 1:i + 1] sma.append(sum(window_slice) / window) return sma def rsi_loop(prices, period=14): gains = [] losses = [] rsi = [None] * len(prices) for i in range(1, len(prices)): change = prices[i] - prices[i - 1] gains.append(max(change, 0)) losses.append(max(-change, 0)) if i >= period: avg_gain = sum(gains[-period:]) / period avg_loss = sum(losses[-period:]) / period if avg_loss == 0: rsi[i] = 100 else: rs = avg_gain / avg_loss rsi[i] = 100 - (100 / (1 + rs)) return rsi
坑点 #1:开头没有用None做填充,会导致索引对不上。
坑点 #2:当列表长度还没到period的时候用sum(gains[-period:]),前面那段的数据全错了。
切到pandas的滚动函数之后,每个指标一行代码就搞定了。而且速度够快,跑实盘tick数据都没问题。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt # 加载数据,假设 df 有一列叫 'close'
df = pd.read_csv('btc_usd_daily.csv', parse_dates=['date'])
df.set_index('date', inplace=True) # ---- 简单移动平均线(SMA)----
window = 20
df['SMA_20'] = df['close'].rolling(window=window).mean() # ---- RSI(相对强弱指数)----
def compute_rsi(series, period=14): delta = series.diff() up = delta.clip(lower=0) down = -delta.clip(upper=0) ma_up = up.ewm(com=period - 1, adjust=False).mean() ma_down = down.ewm(com=period - 1, adjust=False).mean() rs = ma_up / ma_down rsi = 100 - (100 / (1 + rs)) return rsi df['RSI_14'] = compute_rsi(df['close'], period=14) # ---- 信号生成 ----
# 买入条件:价格上穿 SMA 且 RSI 从超卖区(<30)回升
df['signal'] = 0
df.loc[(df['close'] > df['SMA_20']) & (df['RSI_14'] > 30) & (df['close'].shift(1) <= df['SMA_20'].shift(1)) & (df['RSI_14'].shift(1) <= 30), 'signal'] = 1 # 卖出条件:价格下穿 SMA 且 RSI 从超买区(>70)回落
df.loc[(df['close'] < df['SMA_20']) & (df['RSI_14'] < 70) & (df['close'].shift(1) >= df['SMA_20'].shift(1)) & (df['RSI_14'].shift(1) >= 70), 'signal'] = -1 # 绑图
plt.figure(figsize=(12,6))
plt.plot(df['close'], label='收盘价', alpha=0.7)
plt.plot(df['SMA_20'], label='20日均线', alpha=0.9)
plt.scatter(df.index[df['signal']==1], df['close'][df['signal']==1], marker='^', color='g', label='买入', s=100)
plt.scatter(df.index[df['signal']==-1], df['close'][df['signal']==-1], marker='v', color='r', label='卖出', s=100)
plt.title('BTC/USD 与均线、RSI 信号')
plt.legend()
plt.show()
为什么这套方案是真正的升级:
rolling和ewm(指数加权移动平均)自动处理窗口对齐问题——再也不会出现_off-by_one_的幺蛾子。现在你有了可重复、代码驱动的方案来过滤市场上的杂音。不用再对每一个波动疑神疑鬼了,你可以:
最爽的地方在哪?不用迷信付费服务的黑盒指标,从头到尾都是自己写的代码,一行一行攒起来的理解。看到绿色买入箭头跟真实上涨重合的时候——那感觉就像当年背版通关《魂斗罗》一样,货真价实的成就感。
搞一份数据(雅虎财经、币安,或者你自己存的CSV),把上面的代码扔进Jupyter notebook,改一改窗口大小、RSI阈值试试。把SMA换成EMA会怎样?把RSI区间收窄一点,信号会不会更干净?
欢迎在评论区分享你的发现——我也想看看你们在折腾过程中挖到了什么好东西!