From fe3011cfc296fc03081e6c736df64cd72db3cb9f Mon Sep 17 00:00:00 2001 From: vipg Date: Sat, 18 Jul 2026 18:27:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=B7=E5=B7=AE=E5=9B=9E?= =?UTF-8?q?=E6=B5=8B=E3=80=81=E5=AE=8F=E8=A7=82=E6=95=B0=E6=8D=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=92=8C=E4=BA=A4=E6=98=93=E4=BF=A1=E5=8F=B7=E6=A1=86?= =?UTF-8?q?=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- analyze_spread.py | 226 ++++++++++++++++++++++++++++++++++++++++++++++ macro.py | 87 ++++++++++++++++++ trade.py | 59 ++++++++++++ 3 files changed, 372 insertions(+) create mode 100644 analyze_spread.py create mode 100644 macro.py create mode 100644 trade.py diff --git a/analyze_spread.py b/analyze_spread.py new file mode 100644 index 0000000..857782c --- /dev/null +++ b/analyze_spread.py @@ -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)") diff --git a/macro.py b/macro.py new file mode 100644 index 0000000..cb5b6fb --- /dev/null +++ b/macro.py @@ -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 查看当月方向") diff --git a/trade.py b/trade.py new file mode 100644 index 0000000..8ee479c --- /dev/null +++ b/trade.py @@ -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()