diff --git a/spread_chart.html b/spread_chart.html new file mode 100644 index 0000000..9020b19 --- /dev/null +++ b/spread_chart.html @@ -0,0 +1,163 @@ + + + + + +FG 主力合约价差走势 + + + + + +

玻璃期货 — 主力合约价差分析

+

1月 / 5月 / 9月 两两价差(同年配对,远月减近月) | 均值回归信号(60日滚动,2σ 入场)

+ +
+ +
+ +
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/spread_chart.py b/spread_chart.py new file mode 100644 index 0000000..54c1baa --- /dev/null +++ b/spread_chart.py @@ -0,0 +1,402 @@ +""" +主力合约 1月/5月/9月 两两价差走势图 + 均值回归交易信号。 +输出 HTML 图表,浏览器打开即可查看。 + +用法: python3 spread_chart.py +""" +import sqlite3 +import json +import os +from collections import defaultdict +from statistics import mean, stdev + +DB_PATH = os.path.join(os.path.dirname(__file__), "db", "futures.db") +OUT_PATH = os.path.join(os.path.dirname(__file__), "spread_chart.html") + +WINDOW = 60 +ENTRY_Z = 2.0 + + +def build_spreads(): + """构建三组价差序列:01-05, 05-09, 01-09(同年配对)。""" + 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(month_a, month_b, cross_year=False): + per_date = {} + for d in dates: + items = by_date[d] + best_yr = -1 + best = None + for ts_code in items: + if not ts_code.endswith(".ZCE") or not ts_code.startswith("FG"): + continue + yr_str, mon = ts_code[2:4], ts_code[4:6] + if mon == month_a: + yr = int(yr_str) + yr_b_str = str(yr + 1) if cross_year else yr_str + ts_b = f"FG{yr_b_str}{month_b}.ZCE" + if ts_b in items and yr > best_yr: + best_yr = yr + spread = items[ts_b] - items[ts_code] + best = (d, spread) + if best: + per_date[best[0]] = best[1] + + sorted_dates = sorted(per_date.keys()) + return sorted_dates, [per_date[d] for d in sorted_dates] + + return { + "01-05": build("01", "05"), + "05-09": build("05", "09"), + "01-09": build("01", "09"), + } + + +def compute_signals(spread_data): + """计算每个价差的滚动 z-score 和交易信号。""" + month_map = { + "01-05": ("01", "05"), + "05-09": ("05", "09"), + "01-09": ("01", "09"), + } + + signals = {} + for label, (dates, values) in spread_data.items(): + if len(values) < WINDOW + 1: + signals[label] = None + continue + + ma, mb = month_map[label] + recent = values[-WINDOW:] + roll_mean = mean(recent) + roll_std = stdev(recent) + latest = values[-1] + z = (latest - roll_mean) / roll_std if roll_std > 0 else 0 + + if z > ENTRY_Z: + action = f"做空{mb},做多{ma}" + elif z < -ENTRY_Z: + action = f"做多{mb},做空{ma}" + else: + action = "观望" + + z_dates = dates[WINDOW:] + z_values = [] + for i in range(WINDOW, len(values)): + h = values[i - WINDOW : i] + m = mean(h) + s = stdev(h) + z_values.append((values[i] - m) / s if s > 0 else 0) + + signals[label] = { + "latest": latest, + "latest_date": dates[-1], + "mean": round(roll_mean, 1), + "std": round(roll_std, 1), + "z_score": round(z, 2), + "action": action, + "count": len(values), + "z_dates": z_dates, + "z_values": z_values, + } + + return signals + + +def build_chart_data(spread_data, signals): + """构建前端所需的数据结构。""" + all_dates_set = set() + for dates, _ in spread_data.values(): + all_dates_set.update(dates) + all_dates = sorted(all_dates_set) + + colors = { + "01-05": {"line": "#06b6d4", "area": "rgba(6,182,212,0.08)"}, + "05-09": {"line": "#22c55e", "area": "rgba(34,197,94,0.08)"}, + "01-09": {"line": "#f59e0b", "area": "rgba(245,158,11,0.08)"}, + } + + # 主图数据集 + datasets = [] + for label, (dates, values) in spread_data.items(): + date_map = dict(zip(dates, values)) + series = [date_map.get(d) for d in all_dates] + c = colors[label] + datasets.append({ + "label": label, + "data": series, + "borderColor": c["line"], + "backgroundColor": c["area"], + "borderWidth": 1.5, + "pointRadius": 0, + "fill": True, + "tension": 0.1, + }) + + # z-score 数据集 + z_datasets = [] + for label, sig in signals.items(): + if sig is None: + continue + date_map = dict(zip(sig["z_dates"], sig["z_values"])) + series = [date_map.get(d) for d in all_dates] + c = colors[label] + z_datasets.append({ + "label": label, + "data": series, + "borderColor": c["line"], + "backgroundColor": "transparent", + "borderWidth": 1, + "pointRadius": 0, + "tension": 0.1, + }) + + # 统计卡片 + stat_cards = [] + for label, (dates, values) in spread_data.items(): + if not values: + continue + pos = sum(1 for v in values if v > 0) + neg = sum(1 for v in values if v < 0) + stat_cards.append({ + "label": label, + "color": colors[label]["line"], + "count": len(values), + "avg": round(sum(values) / len(values), 1), + "max": max(values), + "min": min(values), + "pos": pos, + "neg": neg, + }) + + # 信号卡片 + sig_cards = [] + for label, sig in signals.items(): + if sig is None: + continue + if "做空" in sig["action"]: + sig_type = "bear" + sig_color = "#ef4444" + elif "做多" in sig["action"]: + sig_type = "bull" + sig_color = "#22c55e" + else: + sig_type = "neutral" + sig_color = "#94a3b8" + sig_cards.append({ + "label": label, + "color": colors[label]["line"], + "sig_type": sig_type, + "sig_color": sig_color, + "action": sig["action"], + "latest": sig["latest"], + "mean": sig["mean"], + "std": sig["std"], + "z_score": sig["z_score"], + "latest_date": sig["latest_date"], + }) + + return { + "all_dates": all_dates, + "datasets": datasets, + "z_datasets": z_datasets, + "stat_cards": stat_cards, + "sig_cards": sig_cards, + } + + +TEMPLATE = r""" + + + + +FG 主力合约价差走势 + + + + + +

玻璃期货 — 主力合约价差分析

+

1月 / 5月 / 9月 两两价差(同年配对,远月减近月) | 均值回归信号(60日滚动,2σ 入场)

+ +
+ +
+ +
+ +
+ +
+ +
+ + + +""" + + +def generate_html(chart_data): + """渲染 HTML。""" + payload = json.dumps({ + "allDates": chart_data["all_dates"], + "datasets": chart_data["datasets"], + "zDatasets": chart_data["z_datasets"], + "statCards": chart_data["stat_cards"], + "sigCards": chart_data["sig_cards"], + }) + html = TEMPLATE.replace("__DATA__", payload) + + with open(OUT_PATH, "w") as f: + f.write(html) + print(f"已生成: {OUT_PATH}") + + +if __name__ == "__main__": + data = build_spreads() + sigs = compute_signals(data) + chart_data = build_chart_data(data, sigs) + generate_html(chart_data)