Files
finance-talk/analyze_spread.py
T
2026-07-18 18:27:41 +08:00

227 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
跨期价差均值回归回测:支持任意合约月份组合。
"""
import sqlite3
from collections import defaultdict
from statistics import mean, stdev
DB_PATH = "/Users/vipg/Documents/futures-data-warehouse/db/futures.db"
conn = sqlite3.connect(DB_PATH)
daily = conn.execute(
"SELECT ts_code, trade_date, close FROM daily WHERE close IS NOT NULL AND close != '' ORDER BY trade_date"
).fetchall()
conn.close()
by_date = defaultdict(dict)
for ts_code, d, close in daily:
by_date[d][ts_code] = float(close)
dates = sorted(by_date.keys())
def build_spread(month_a, month_b, cross_year=False):
"""
构建两合约月份的价差序列。
month_a, month_b: '01','05','09'
cross_year: True 表示 year_b = year_a + 1(如 09-01 跨年)
返回 [(date, ts_a, ts_b, p_a, p_b, spread)]
"""
data = []
for d in dates:
items = by_date[d]
for ts_code in list(items.keys()):
if not ts_code.endswith(".ZCE") or not ts_code.startswith("FG"):
continue
yr, mon = ts_code[2:4], ts_code[4:6]
if mon == month_a:
yr_b = str(int(yr) + 1) if cross_year else yr
ts_b = f"FG{yr_b}{month_b}.ZCE"
if ts_b in items:
p_a = items[ts_code]
p_b = items[ts_b]
spread = p_b - p_a
data.append((d, ts_code, ts_b, p_a, p_b, spread))
return data
def backtest_spread(spread_data, window=60, entry_z=2.0, exit_z=0.5, max_hold=20):
equity = 1.0
position = 0
entry_spread = 0
entry_p_a = 0
entry_date = ""
hold_days = 0
trades = []
daily_eq = []
for i, (d, ts_a, ts_b, p_a, p_b, spread_val) in enumerate(spread_data):
if i < window:
daily_eq.append((d, equity))
continue
hist = [spread_data[j][5] for j in range(i - window, i)]
roll_mean = mean(hist)
roll_std = stdev(hist)
signal = 0
if spread_val > roll_mean + entry_z * roll_std:
signal = -1 # 价差过大 → 空价差(空远月、多近月)
elif spread_val < roll_mean - entry_z * roll_std:
signal = 1
should_exit = False
if position != 0:
hold_days += 1
if hold_days >= max_hold:
should_exit = True
if position == 1 and spread_val >= roll_mean - exit_z * roll_std:
should_exit = True
elif position == -1 and spread_val <= roll_mean + exit_z * roll_std:
should_exit = True
if position != 0 and should_exit:
if position == 1:
pnl = (spread_val - entry_spread) / entry_p_a
else:
pnl = (entry_spread - spread_val) / entry_p_a
equity *= (1 + pnl)
trades.append((entry_date, d, entry_spread, spread_val, pnl, position))
position = 0
hold_days = 0
if position == 0 and signal != 0:
position = signal
entry_spread = spread_val
entry_p_a = p_a
entry_date = d
hold_days = 0
daily_eq.append((d, equity))
if position != 0 and spread_data:
d, _, _, p_a, _, spread_val = spread_data[-1]
if position == 1:
pnl = (spread_val - entry_spread) / entry_p_a
else:
pnl = (entry_spread - spread_val) / entry_p_a
equity *= (1 + pnl)
trades.append((entry_date, d, entry_spread, spread_val, pnl, position))
return trades, daily_eq
def print_results(trades, daily_eq, label):
if not trades:
print(f"\n{label}: 无交易")
return
wins = [t for t in trades if t[4] > 0]
losses = [t for t in trades if t[4] <= 0]
total_ret = daily_eq[-1][1] - 1.0 if daily_eq else 0
wr = len(wins) / len(trades) if trades else 0
returns = []
for i in range(1, len(daily_eq)):
if daily_eq[i-1][1] > 0:
returns.append(daily_eq[i][1] / daily_eq[i-1][1] - 1)
avg_ret = mean(returns) if returns else 0
std_ret = stdev(returns) if len(returns) > 1 else 1
sharpe = (avg_ret / std_ret) * (252 ** 0.5) if std_ret > 0 else 0
peak = 1.0
mdd = 0.0
for _, e in daily_eq:
if e > peak: peak = e
dd = (peak - e) / peak
if dd > mdd: mdd = dd
print(f"\n{label}")
print(f" {'' if trades else '无'}交易次数: {len(trades)}")
print(f" 总收益率: {total_ret:+.2%}")
print(f" 夏普比率: {sharpe:.2f}")
print(f" 最大回撤: {mdd:.2%}")
print(f" 胜率: {wr:.0%}")
if losses:
avg_w = mean(t[4] for t in wins)
avg_l = mean(t[4] for t in losses)
print(f" 盈亏比: {abs(avg_w/avg_l):.2f}")
# ── 分析所有有效组合 ──────────────────────────
pairs = [
("01", "05", False, "同一年 01-05"),
("05", "09", False, "同一年 05-09"),
("09", "01", True, "跨年 09-01"),
]
# 先看各组合的统计特征
print("=" * 60)
print("各合约组合价差统计")
print("=" * 60)
for ma, mb, cross, label in pairs:
data = build_spread(ma, mb, cross)
n = len(data)
vals = [r[5] for r in data]
pos = sum(1 for v in vals if v > 0)
avg_s = mean(vals)
std_s = stdev(vals)
print(f"\n{label:>12} {n:>5}天 均值{avg_s:>+7.1f} σ{std_s:>6.1f}{pos:>4}({pos/n:.0%})")
# 回测各组合
print("\n" + "=" * 60)
print("均值回归回测 (window=60 entry=2σ exit=0.5σ max_hold=20)")
print("=" * 60)
for ma, mb, cross, label in pairs:
data = build_spread(ma, mb, cross)
trades, eq = backtest_spread(data, window=60, entry_z=2.0, exit_z=0.5, max_hold=20)
print_results(trades, eq, label)
# 当前可交易组合 (202607)
print("\n" + "=" * 60)
print("当前可交易组合分析 (FG2609, FG2701, FG2705)")
print("=" * 60)
today = "20260717"
items = by_date.get(today, {})
for ts in ["FG2609.ZCE", "FG2701.ZCE", "FG2705.ZCE"]:
if ts in items:
print(f" {ts}: {items[ts]}")
else:
print(f" {ts}: 无当日数据")
# 当前配对及统计
pairs_now = [
("FG2609.ZCE", "FG2701.ZCE", "09-01(跨年)"),
("FG2701.ZCE", "FG2705.ZCE", "01-05(同一年)"),
("FG2609.ZCE", "FG2705.ZCE", "09-05(跨年)"),
]
for ts_a, ts_b, label in pairs_now:
if ts_a not in items or ts_b not in items:
print(f"\n{label}: {ts_a}{ts_b} 无数据")
continue
spread_now = items[ts_b] - items[ts_a]
print(f"\n{label}: {ts_a} vs {ts_b}")
print(f" 当前价差: {spread_now:+.1f}")
# 找历史均值
for ma, mb, cross, _ in pairs:
if (ts_a[4:6] == ma and ts_b[4:6] == mb) or \
(ts_a[4:6] == mb and ts_b[4:6] == ma and cross):
data = build_spread(ma, mb, cross)
if len(data) < 60:
continue
vals = [r[5] for r in data]
# 近 60 天
recent = vals[-60:]
avg_r = mean(recent)
std_r = stdev(recent)
z = (spread_now - avg_r) / std_r if std_r else 0
print(f" 近60日均值: {avg_r:.1f} σ: {std_r:.1f} z值: {z:.2f}")
if abs(z) >= 2:
print(f" → 偏离 {z:.0f}σ,触发信号!")
else:
print(f" → 未触发 (需 |z|>=2)")