量化工具:Python实现基于库存的基差交易信号生成
说实话,很多做基差交易的朋友,都卡在「信号生成」这一步。理论都懂——库存高,基差弱;库存低,基差强。但真要把这个逻辑写成代码,跑出可执行的交易信号,又是另一回事了。
我个人习惯,是把库存数据当作「核心因子」,然后用Python把它量化成具体的买卖信号。今天我就带你走一遍这个流程。嗯,代码不多,但每一步都有坑。
1. 核心逻辑:库存与基差的「反向联动」
先理清思路。基差 = 现货价格 - 期货价格。库存高的时候,现货往往供过于求,价格承压,基差走弱(甚至变成负的)。库存低的时候,现货紧缺,基差走强。
所以我们的信号生成逻辑很简单:
- 库存处于历史低位 → 预期基差走强 → 做多基差(买现货,卖期货)
- 库存处于历史高位 → 预期基差走弱 → 做空基差(卖现货,买期货)
- 库存处于中性区间 → 观望,不产生信号
你想想看,这个逻辑听起来简单,但实际跑起来,阈值怎么设?数据怎么标准化?我一开始也踩过坑。
关键点: 库存数据必须做「历史分位数」处理,不能直接用绝对值。因为不同品种、不同年份的库存水平差异巨大。
2. 数据准备:你需要什么?
要生成信号,至少需要两类数据:
| 数据类别 | 字段 | 频率 |
|---|---|---|
| 库存数据 | 日期、品种、库存量 | 周度/日度 |
| 基差数据 | 日期、品种、基差值 | 日度 |
我在项目中遇到过一个问题:库存数据经常有缺失,尤其是节假日前后。如果你直接用空值去算分位数,结果会偏得离谱。我的做法是——先做前向填充,再算滚动分位数。
3. Python代码实现:信号生成引擎
下面这段代码,是我自己一直在用的一个简化版。它把库存数据转成「库存分位数」,然后根据分位数生成交易信号。
import pandas as pd
import numpy as np
def generate_inventory_signal(inventory_series, lookback=252, low_threshold=0.2, high_threshold=0.8):
"""
基于库存分位数生成基差交易信号
参数:
- inventory_series: 库存时间序列 (pandas Series)
- lookback: 滚动窗口长度,默认252个交易日
- low_threshold: 低位阈值,库存分位数低于此值做多
- high_threshold: 高位阈值,库存分位数高于此值做空
返回:
- signal: 1=做多基差, -1=做空基差, 0=观望
"""
# 计算滚动分位数
rolling_rank = inventory_series.rolling(window=lookback, min_periods=100).apply(
lambda x: pd.Series(x).rank(pct=True).iloc[-1]
)
# 生成信号
signal = pd.Series(0, index=inventory_series.index)
signal[rolling_rank <= low_threshold] = 1 # 库存低位,做多基差
signal[rolling_rank >= high_threshold] = -1 # 库存高位,做空基差
return signal
# 使用示例
# inventory_data = pd.read_csv('inventory.csv', index_col='date', parse_dates=True)
# signal = generate_inventory_signal(inventory_data['inventory'])
我的习惯: lookback 参数我一般设252(一年交易日),但如果你做的是短线交易,可以缩到60或90。阈值0.2和0.8是经验值,你可以根据品种调整。
4. 信号过滤:别让噪音干扰你
直接跑上面的代码,你会发现信号切换非常频繁。今天做多,明天就变做空了。这在实际交易中根本没法执行。
我曾经吃过这个亏。2019年做螺纹钢,信号一天变三次,手续费都亏麻了。后来我加了一个「信号持久化」的过滤机制:
def filter_signal(signal, min_hold_days=5):
"""
信号过滤:信号至少保持 min_hold_days 天不变
"""
filtered = signal.copy()
last_signal = 0
hold_count = 0
for i in range(len(signal)):
if signal.iloc[i] != last_signal:
if hold_count >= min_hold_days:
last_signal = signal.iloc[i]
hold_count = 0
else:
# 信号持续时间不够,保持原信号
filtered.iloc[i] = last_signal
hold_count += 1
else:
hold_count += 1
return filtered
说白了,这个过滤就是让信号「钝化」一点。你想想看,库存变化是缓慢的,基差变化也是缓慢的,信号没必要那么灵敏。
5. 可视化:看一眼就知道信号质量
光有数字信号不够,我习惯画一张图,把库存分位数、基差、信号三条线叠在一起看。这样一眼就能判断信号是否合理。
import matplotlib.pyplot as plt
def plot_signal(inventory_pct, basis, signal):
fig, ax1 = plt.subplots(figsize=(12, 6))
ax1.plot(inventory_pct.index, inventory_pct, label='库存分位数', color='blue', alpha=0.7)
ax1.axhline(y=0.2, color='gray', linestyle='--', alpha=0.5)
ax1.axhline(y=0.8, color='gray', linestyle='--', alpha=0.5)
ax1.set_ylabel('库存分位数')
ax2 = ax1.twinx()
ax2.plot(basis.index, basis, label='基差', color='orange', alpha=0.7)
ax2.set_ylabel('基差')
# 标记信号
for i in range(len(signal)):
if signal.iloc[i] == 1:
ax1.axvline(x=signal.index[i], color='green', alpha=0.1)
elif signal.iloc[i] == -1:
ax1.axvline(x=signal.index[i], color='red', alpha=0.1)
plt.title('库存分位数 vs 基差 vs 交易信号')
plt.show()
注意: 可视化只是辅助工具。别因为图好看就盲目信任信号。我见过太多人,看到信号和历史回测曲线漂亮,就直接上实盘,结果亏得很惨。
6. 核心知识体系:一张图说清楚
下面这张SVG图,把整个信号生成的流程串起来了。从数据输入到信号输出,每一步都离不开。
7. 避坑指南:我踩过的三个大坑
- 库存数据口径不一致——我曾经同时用了两个数据源的库存,一个算的是「社会库存」,另一个算的是「厂库」,结果信号完全相反。后来我统一只用交易所公布的仓单数据。
- 阈值设得太死——0.2和0.8不是万能药。有些品种波动大,0.2可能一年都触发不了几次。我建议你先跑一遍历史数据,看看分位数的分布,再动态调整。
- 忽略基差的绝对值——库存分位数低,但基差已经很高了,这时候做多基差可能追高。我后来加了一个「基差绝对水平」的过滤,基差处于历史高位时,即使库存低也不做多。
总结一下: 基于库存的基差交易信号,核心就是「库存分位数 + 阈值判断 + 信号过滤」。代码不难,难的是参数调优和风控。别想着一步到位,先跑模拟盘跑三个月再说。
好了,这一章的内容就到这里。代码你可以直接拿去用,但记得根据自己的品种调整参数。有什么问题,欢迎交流。