新增价差回测、宏观数据管理和交易信号框架
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
跨期价差均值回归回测:支持任意合约月份组合。
|
||||||
|
"""
|
||||||
|
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)")
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
宏观数据管理:记录和管理月频地产景气度数据。
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_PATH = "/Users/vipg/Documents/futures-data-warehouse/db/futures.db"
|
||||||
|
|
||||||
|
TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS macro (
|
||||||
|
month TEXT PRIMARY KEY,
|
||||||
|
housing_start_yoy REAL,
|
||||||
|
note TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.execute(TABLE_SQL)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def add(month, yoy, note=""):
|
||||||
|
"""添加/更新一个月的地产数据"""
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO macro (month, housing_start_yoy, note) VALUES (?, ?, ?)",
|
||||||
|
(month, yoy, note),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print(f"macro: {month} 新房开工同比 {yoy:+.1f}% {note}")
|
||||||
|
|
||||||
|
|
||||||
|
def list_all():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT month, housing_start_yoy, note FROM macro ORDER BY month DESC"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
if not rows:
|
||||||
|
print("暂无宏观数据")
|
||||||
|
return
|
||||||
|
print(f"{'月份':>8} {'新房开工同比':>10} {'备注'}")
|
||||||
|
print("-" * 40)
|
||||||
|
for r in rows:
|
||||||
|
print(f"{r[0]:>8} {r[1]:>+9.1f}% {r[2] or ''}")
|
||||||
|
|
||||||
|
|
||||||
|
def direction():
|
||||||
|
"""根据最新宏观数据返回当月方向偏好"""
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT month, housing_start_yoy FROM macro ORDER BY month DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return "观望", "无宏观数据"
|
||||||
|
|
||||||
|
month, yoy = row
|
||||||
|
if yoy > 5:
|
||||||
|
return "做多", f"{month} 开工同比 {yoy:+.1f}%,景气回暖"
|
||||||
|
elif yoy > -5:
|
||||||
|
return "观望", f"{month} 开工同比 {yoy:+.1f}%,方向不明"
|
||||||
|
else:
|
||||||
|
return "做空", f"{month} 开工同比 {yoy:+.1f}%,持续低迷"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init()
|
||||||
|
|
||||||
|
if len(sys.argv) >= 3 and sys.argv[1] == "add":
|
||||||
|
add(sys.argv[2], float(sys.argv[3]), " ".join(sys.argv[4:]))
|
||||||
|
elif len(sys.argv) >= 2 and sys.argv[1] == "list":
|
||||||
|
list_all()
|
||||||
|
elif len(sys.argv) >= 2 and sys.argv[1] == "direction":
|
||||||
|
d, reason = direction()
|
||||||
|
print(f"当前方向: {d}")
|
||||||
|
print(f"依据: {reason}")
|
||||||
|
else:
|
||||||
|
print("用法:")
|
||||||
|
print(" python3 macro.py add 202606 -26.0 6月数据")
|
||||||
|
print(" python3 macro.py list 查看所有")
|
||||||
|
print(" python3 macro.py direction 查看当月方向")
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
每日交易信号汇总。
|
||||||
|
开盘前跑一下,输出当天方向建议。
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from macro import direction as macro_direction, init as macro_init
|
||||||
|
|
||||||
|
DB_PATH = "/Users/vipg/Documents/futures-data-warehouse/db/futures.db"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
macro_init()
|
||||||
|
|
||||||
|
today = datetime.now().strftime("%Y%m%d")
|
||||||
|
|
||||||
|
# 1. 宏观方向
|
||||||
|
dir, reason = macro_direction()
|
||||||
|
print(f"日期: {today}")
|
||||||
|
print(f"宏观方向: {dir}")
|
||||||
|
print(f"依据: {reason}")
|
||||||
|
|
||||||
|
if dir == "观望":
|
||||||
|
print("\n建议: 今天不交易")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 持仓量最大的主力合约
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
main_row = conn.execute("""
|
||||||
|
SELECT d.ts_code, d.open, d.close, d.high, d.low, d.oi
|
||||||
|
FROM daily d
|
||||||
|
JOIN contracts c ON d.ts_code = c.ts_code
|
||||||
|
WHERE d.trade_date = ? AND c.delist_date > ? AND d.oi > 0
|
||||||
|
ORDER BY d.oi DESC LIMIT 1
|
||||||
|
""", (today, today)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if main_row:
|
||||||
|
ts, open_p, close_p, high, low, oi = main_row
|
||||||
|
print(f"\n主力合约: {ts}")
|
||||||
|
print(f"昨收: {close_p} 持仓: {int(oi):,}")
|
||||||
|
else:
|
||||||
|
print("\n注意: 今日行情数据尚未拉取,请先运行 update.py")
|
||||||
|
print("或使用历史近似的合约数据")
|
||||||
|
|
||||||
|
# 3. 交易提醒
|
||||||
|
print(f"\n{'='*40}")
|
||||||
|
print(f"今日计划: {dir}")
|
||||||
|
print(f" 开盘后确认方向后入场")
|
||||||
|
print(f" 止损: 5跳")
|
||||||
|
print(f" 收盘前5分钟平仓")
|
||||||
|
print(f"{'='*40}")
|
||||||
|
print(f"\n风控提醒:")
|
||||||
|
print(f" - 单笔止损严格执行 5 跳")
|
||||||
|
print(f" - 连续亏损 3 天暂停交易")
|
||||||
|
print(f" - 累计盈利达标后买入期权做保护")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user