第二十讲:可视化分析——让数据自己说话
做量化交易,尤其是基差交易和跨品种套利,最怕什么?
怕数据骗人,怕策略失效,怕回测结果看着漂亮,实盘一跑就崩。
我个人的经验是——可视化是最好的验尸官。它能把策略的每个毛孔都放大给你看。今天这一讲,我们就来聊聊怎么用Matplotlib、Plotly这些工具,把价差、K线、回测曲线画得明明白白。
核心观点:可视化不是装饰,是诊断工具。一张好图,胜过千行回测日志。
20.1 为什么可视化对套利策略如此重要?
你想想看,基差交易和跨品种套利,本质上是在交易「价差」的均值回归或趋势延续。价差本身是个衍生指标,它不像股价那样直观。如果只看数字,你很难发现:
- 价差是否真的在某个区间内震荡?
- 有没有出现极端值?是数据错误还是市场异动?
- 策略开仓点是否踩在了价差的拐点上?
我在项目中遇到过一回,回测报告显示年化收益30%,我差点就准备上实盘了。结果画了个价差图一看——好家伙,80%的盈利来自一次数据缺失导致的假信号。从那以后,我养成了习惯:任何策略上线前,必须过一遍可视化检查清单。
20.2 Matplotlib:经典价差图与回测曲线
Matplotlib是Python可视化的老大哥。虽然它交互性差,但胜在稳定、可控、出版级质量。我个人习惯用它来做静态分析报告。
20.2.1 绘制价差图
价差图是套利策略的「心电图」。下面这段代码,展示了如何画出两个品种的价差,并标注开平仓点:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# 模拟数据:螺纹钢和热卷的价差
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=200, freq='D')
spread = np.cumsum(np.random.randn(200) * 0.5) + 10 # 模拟价差
# 生成交易信号:价差超过±2倍标准差开仓
mean_spread = np.mean(spread)
std_spread = np.std(spread)
upper = mean_spread + 2 * std_spread
lower = mean_spread - 2 * std_spread
# 找开仓点
long_signals = spread < lower
short_signals = spread > upper
plt.figure(figsize=(12, 6))
plt.plot(dates, spread, label='价差', color='blue', linewidth=1.5)
plt.axhline(mean_spread, color='gray', linestyle='--', alpha=0.7, label='均值')
plt.axhline(upper, color='red', linestyle='--', alpha=0.5, label='上轨')
plt.axhline(lower, color='green', linestyle='--', alpha=0.5, label='下轨')
# 标注开仓点
plt.scatter(dates[long_signals], spread[long_signals],
marker='^', color='green', s=100, label='做多开仓', zorder=5)
plt.scatter(dates[short_signals], spread[short_signals],
marker='v', color='red', s=100, label='做空开仓', zorder=5)
plt.title('螺纹钢-热卷价差与交易信号', fontsize=14)
plt.xlabel('日期')
plt.ylabel('价差')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
避坑指南:我曾经在画价差图时,忘记对数据进行对齐。两个品种的交易时间不同,导致价差出现「假突破」。后来我强制要求:画图前必须用reindex()对齐时间戳。
20.2.2 回测曲线展示
回测曲线是策略的「成绩单」。我习惯用对数坐标展示资金曲线,这样能更清楚地看到复利效果:
# 模拟回测净值
np.random.seed(123)
equity = 100 * np.exp(np.cumsum(np.random.randn(500) * 0.02 + 0.001))
plt.figure(figsize=(12, 6))
plt.plot(equity, color='darkblue', linewidth=2, label='策略净值')
plt.yscale('log') # 对数坐标
plt.title('策略回测净值曲线(对数坐标)', fontsize=14)
plt.xlabel('交易次数')
plt.ylabel('净值(对数)')
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
注意:回测曲线不要只看终点收益。我建议你同时画出最大回撤区间和夏普比率滚动窗口。有一次我发现一个策略年化收益40%,但最大回撤高达35%——这种策略实盘根本拿不住。
20.3 Plotly:交互式图表让分析更深入
Matplotlib是静态的,Plotly是活的。你可以缩放、悬停、筛选。我个人觉得,探索性数据分析阶段,Plotly比Matplotlib好用十倍。
20.3.1 交互式价差图
下面这个例子,展示了如何用Plotly画出可交互的价差图。鼠标悬停就能看到具体数值:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# 创建交互式图表
fig = go.Figure()
# 添加价差线
fig.add_trace(go.Scatter(
x=dates, y=spread,
mode='lines',
name='价差',
line=dict(color='blue', width=2),
hovertemplate='日期: %{x|%Y-%m-%d}<br>价差: %{y:.2f}<extra></extra>'
))
# 添加上下轨
fig.add_hline(y=upper, line_dash="dash", line_color="red",
annotation_text="上轨")
fig.add_hline(y=lower, line_dash="dash", line_color="green",
annotation_text="下轨")
fig.update_layout(
title='交互式价差图(可缩放)',
xaxis_title='日期',
yaxis_title='价差',
hovermode='x unified',
template='plotly_white'
)
fig.show()
小技巧:Plotly的hovermode='x unified'可以让你同时看到所有曲线在同一个时间点的数值。这在分析多品种价差时特别有用。
20.4 K线图与指标叠加
做跨品种套利,光看价差不够,还得看单个品种的K线形态。我习惯用mplfinance库来画K线,它比Matplotlib原生画法方便太多。
20.4.1 用mplfinance画K线
import mplfinance as mpf
# 模拟K线数据
dates_k = pd.date_range('2024-01-01', periods=100, freq='D')
data = pd.DataFrame({
'Open': np.random.randn(100).cumsum() + 100,
'High': np.random.randn(100).cumsum() + 102,
'Low': np.random.randn(100).cumsum() + 98,
'Close': np.random.randn(100).cumsum() + 100,
'Volume': np.random.randint(1000, 5000, 100)
}, index=dates_k)
# 添加移动平均线
mpf.plot(data, type='candle', mav=(5, 20), volume=True,
style='charles', title='螺纹钢主力合约K线',
ylabel='价格', ylabel_lower='成交量')
20.4.2 叠加技术指标
我经常在K线图上叠加RSI或布林带,用来辅助判断开仓时机:
# 计算RSI
def calc_rsi(series, period=14):
delta = series.diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(period).mean()
avg_loss = loss.rolling(period).mean()
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
data['RSI'] = calc_rsi(data['Close'])
# 创建子图:K线 + RSI
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.1,
row_heights=[0.7, 0.3])
# 上部分:K线
fig.add_trace(go.Candlestick(
x=data.index,
open=data['Open'], high=data['High'],
low=data['Low'], close=data['Close'],
name='K线'
), row=1, col=1)
# 下部分:RSI
fig.add_trace(go.Scatter(
x=data.index, y=data['RSI'],
mode='lines', name='RSI',
line=dict(color='purple')
), row=2, col=1)
# 添加RSI超买超卖线
fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1)
fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1)
fig.update_layout(title='K线 + RSI指标叠加', showlegend=False)
fig.show()
注意:指标叠加不是越多越好。我见过有人一个图上叠了8个指标,结果什么都看不清。我的原则是:一个图最多叠2-3个指标,且必须逻辑相关。
20.5 本章知识体系
下面这张SVG图,总结了本章的核心逻辑:
20.6 实战建议与避坑指南
最后,分享几个我踩过的坑:
- 数据对齐是第一步——我曾经因为两个品种的交易日历不同,画出来的价差图全是锯齿。后来我强制用
pd.merge(asof=True)做前向填充。 - 不要只看收盘价——做日内套利时,用收盘价画图会丢失大量信息。我建议至少用1小时K线。
- 回测曲线要画「最大回撤阴影」——用
plt.fill_between把回撤区间标出来,一眼就能看出策略的脆弱期。 - 交互式图表要导出为HTML——Plotly的图表可以保存为独立HTML文件,方便分享给团队。用
fig.write_html('report.html')即可。
一句话总结:可视化不是画图,是思考。你画得越仔细,策略的漏洞暴露得越彻底。