From b3c46a512f29dd99c380772e23c89c1ab50f28ff Mon Sep 17 00:00:00 2001 From: fish Date: Mon, 20 Jul 2026 09:54:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=89=80=E6=9C=89=20Python?= =?UTF-8?q?=20=E8=84=9A=E6=9C=AC=E5=92=8C=20HTML=20=E5=9B=BE=E8=A1=A8?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyze_spread.py | 226 -------------------------- chart.html | 121 -------------- chart.py | 149 ----------------- macro.py | 87 ---------- quote.py | 87 ---------- spread_chart.html | 163 ------------------- spread_chart.py | 402 ---------------------------------------------- trade.py | 59 ------- update.py | 351 ---------------------------------------- 9 files changed, 1645 deletions(-) delete mode 100644 analyze_spread.py delete mode 100644 chart.html delete mode 100644 chart.py delete mode 100644 macro.py delete mode 100644 quote.py delete mode 100644 spread_chart.html delete mode 100644 spread_chart.py delete mode 100644 trade.py delete mode 100644 update.py diff --git a/analyze_spread.py b/analyze_spread.py deleted file mode 100644 index 857782c..0000000 --- a/analyze_spread.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -跨期价差均值回归回测:支持任意合约月份组合。 -""" -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/chart.html b/chart.html deleted file mode 100644 index 4cb0bc7..0000000 --- a/chart.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - -玻璃期货 spread 走势 - - - - -

玻璃期货 — 合约链差价 & 主力合约日内涨跌

- -
- -
- -
- - - - \ No newline at end of file diff --git a/chart.py b/chart.py deleted file mode 100644 index 62d4cd1..0000000 --- a/chart.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -生成 spread 数据图表(HTML)。 -用法: python3 chart.py -""" -import sqlite3 -import json - -DB_PATH = "/Users/vipg/Documents/futures-data-warehouse/db/futures.db" - -def main(): - conn = sqlite3.connect(DB_PATH) - rows = conn.execute( - "SELECT trade_date, spread, main_chg FROM spread ORDER BY trade_date" - ).fetchall() - conn.close() - - dates = [r[0] for r in rows] - spreads = [r[1] for r in rows] - chgs = [r[2] if r[2] is not None else None for r in rows] - - html = f""" - - - - -玻璃期货 spread 走势 - - - - -

玻璃期货 — 合约链差价 & 主力合约日内涨跌

- -
- -
- -
- - - -""" - - out_path = "/Users/vipg/Documents/futures-data-warehouse/chart.html" - with open(out_path, "w") as f: - f.write(html) - print(f"已生成: {out_path}") - -if __name__ == "__main__": - main() diff --git a/macro.py b/macro.py deleted file mode 100644 index cb5b6fb..0000000 --- a/macro.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -宏观数据管理:记录和管理月频地产景气度数据。 -""" -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/quote.py b/quote.py deleted file mode 100644 index 1e28ac4..0000000 --- a/quote.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -查询指定交易日玻璃期货主力合约行情。 -用法: python3 quote.py 20260717 -""" -import sqlite3 -import sys -from datetime import datetime - -DB_PATH = "/Users/vipg/Documents/futures-data-warehouse/db/futures.db" - -def main(): - if len(sys.argv) < 2: - print("用法: python3 quote.py YYYYMMDD") - sys.exit(1) - - date_str = sys.argv[1] - try: - datetime.strptime(date_str, "%Y%m%d") - except ValueError: - print(f"日期格式错误: {date_str},应为 YYYYMMDD") - sys.exit(1) - - conn = sqlite3.connect(DB_PATH) - - count = conn.execute( - "SELECT COUNT(*) FROM daily WHERE trade_date=?", (date_str,) - ).fetchone()[0] - if count == 0: - import urllib.request, json - req_data = json.dumps({ - "api_name": "trade_cal", - "token": "76efd8465f9f2591aa42a385268e06acf6b80b7a15be2267ad2281b7", - "params": {"exchange": "CZCE", "start_date": date_str, "end_date": date_str}, - "fields": "cal_date,is_open", - }).encode() - resp = json.loads(urllib.request.urlopen( - urllib.request.Request("https://api.tushare.pro", data=req_data, - headers={"Content-Type": "application/json"}) - ).read()) - is_open = resp.get("data", {}).get("items", [[None, None]])[0][1] - if is_open == 1: - print(f"{date_str} 是交易日,但数据库尚无数据(需先运行 update.py 拉取)") - elif is_open == 0: - print(f"{date_str} 非交易日") - else: - print(f"{date_str} 无数据") - conn.close() - sys.exit(1) - - rows = conn.execute(""" - SELECT d.ts_code, d.open, d.high, d.low, d.close - 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 c.delist_date - """, (date_str, date_str)).fetchall() - - if not rows: - print(f"{date_str} 无活跃合约数据") - conn.close() - sys.exit(1) - - print(f"\n{date_str} 玻璃期货") - print(f"{'合约':>12} 收盘价 差价") - print("-" * 34) - prev = None - total_diff = 0.0 - for r in rows: - close = float(r[4]) if r[4] is not None and r[4] != "" else None - close_str = f"{close:.1f}" if close else "-" - diff = "" - if close is not None and prev is not None: - d = close - prev - diff = f"{d:+.1f}" - total_diff += d - print(f"{r[0]:>12} {close_str:>7} {diff:>6}") - if close is not None: - prev = close - print("-" * 34) - print(f"{'差价合计':>20} {total_diff:+.1f}") - - conn.close() - -if __name__ == "__main__": - main() diff --git a/spread_chart.html b/spread_chart.html deleted file mode 100644 index 9020b19..0000000 --- a/spread_chart.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - -FG 主力合约价差走势 - - - - - -

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

-

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

- -
- -
- -
- -
- -
- -
- - - - \ No newline at end of file diff --git a/spread_chart.py b/spread_chart.py deleted file mode 100644 index 54c1baa..0000000 --- a/spread_chart.py +++ /dev/null @@ -1,402 +0,0 @@ -""" -主力合约 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) diff --git a/trade.py b/trade.py deleted file mode 100644 index 8ee479c..0000000 --- a/trade.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -每日交易信号汇总。 -开盘前跑一下,输出当天方向建议。 -""" -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() diff --git a/update.py b/update.py deleted file mode 100644 index 1902089..0000000 --- a/update.py +++ /dev/null @@ -1,351 +0,0 @@ -import csv -import os -import sqlite3 -import sys -import time -from datetime import datetime - -import requests - -TOKEN = "76efd8465f9f2591aa42a385268e06acf6b80b7a15be2267ad2281b7" -API_URL = "https://api.tushare.pro" -DATA_DIR = os.path.join(os.path.dirname(__file__), "data") -DB_PATH = os.path.join(os.path.dirname(__file__), "db", "futures.db") - -FUT_DAILY_FIELDS = [ - "ts_code", "trade_date", "pre_close", "pre_settle", - "open", "high", "low", "close", "settle", - "change1", "change2", "vol", "amount", "oi", "oi_chg", - "delv_settle", -] - - -def tushare_query(api_name, params=None, fields=None): - req = {"api_name": api_name, "token": TOKEN} - if params: - req["params"] = params - if fields: - req["fields"] = fields - resp = requests.post(API_URL, json=req) - data = resp.json() - if data["code"] != 0: - raise Exception(f"API error ({data['code']}): {data['msg']}") - fields_list = data["data"]["fields"] - items = data["data"]["items"] - return [dict(zip(fields_list, item)) for item in items] - - -def is_trading_day(exchange="CZCE"): - today = datetime.now().strftime("%Y%m%d") - rows = tushare_query( - "trade_cal", - params={"exchange": exchange, "start_date": today, "end_date": today}, - fields="cal_date,is_open", - ) - if rows: - return rows[0]["is_open"] == "1" - return True - - -# ── 合约列表 ────────────────────────────────── - -def get_active_contracts(exchange, fut_code): - contracts = tushare_query( - "fut_basic", - params={"exchange": exchange, "fut_code": fut_code, "fut_type": "1"}, - fields="ts_code,symbol,name,list_date,delist_date", - ) - today = datetime.now().strftime("%Y%m%d") - return [c for c in contracts if not c.get("delist_date") or c["delist_date"] > today] - - -def get_all_contracts(exchange, fut_code): - return tushare_query( - "fut_basic", - params={"exchange": exchange, "fut_code": fut_code, "fut_type": "1"}, - fields="ts_code,symbol,name,list_date,delist_date", - ) - - -# ── 增量更新 CSV ────────────────────────────── - -def update_contract_csv(fut_code, contract): - ts_code = contract["ts_code"] - csv_name = ts_code.split(".")[0] + ".csv" - out_dir = os.path.join(DATA_DIR, fut_code) - csv_path = os.path.join(out_dir, csv_name) - os.makedirs(out_dir, exist_ok=True) - - existing = [] - if os.path.exists(csv_path): - with open(csv_path) as f: - reader = csv.DictReader(f) - existing = list(reader) - - last_date = existing[-1]["trade_date"] if existing else None - - params = {"ts_code": ts_code} - if last_date: - params["start_date"] = last_date - - rows = tushare_query("fut_daily", params=params, fields=",".join(FUT_DAILY_FIELDS)) - if not rows: - return None - - rows.sort(key=lambda r: r["trade_date"]) - - if not existing: - with open(csv_path, "w", newline="") as f: - w = csv.DictWriter(f, fieldnames=FUT_DAILY_FIELDS) - w.writeheader() - w.writerows(rows) - return rows - - if rows[0]["trade_date"] == last_date: - existing = existing[:-1] - merged = existing + rows - else: - merged = existing + rows - - with open(csv_path, "w", newline="") as f: - w = csv.DictWriter(f, fieldnames=FUT_DAILY_FIELDS) - w.writeheader() - w.writerows(merged) - - return rows - - -def update_all_contracts(fut_code, contracts): - all_new = [] - for c in contracts: - code = c["ts_code"].split(".")[0] - new_rows = update_contract_csv(fut_code, c) - if new_rows is None: - print(f" {code:12s} 无新数据") - else: - print(f" {code:12s} +{len(new_rows)} 条 ({new_rows[0]['trade_date']} ~ {new_rows[-1]['trade_date']})") - all_new.extend(new_rows) - time.sleep(0.3) - return all_new - - -# ── 全量拉取(初始化用) ───────────────────── - -def fetch_all_csv(fut_code, contracts): - out_dir = os.path.join(DATA_DIR, fut_code) - os.makedirs(out_dir, exist_ok=True) - for c in contracts: - ts_code = c["ts_code"] - csv_name = ts_code.split(".")[0] + ".csv" - csv_path = os.path.join(out_dir, csv_name) - rows = tushare_query( - "fut_daily", params={"ts_code": ts_code}, fields=",".join(FUT_DAILY_FIELDS), - ) - if not rows: - print(f" {csv_name:12s} 无数据,跳过") - continue - rows.sort(key=lambda r: r["trade_date"]) - with open(csv_path, "w", newline="") as f: - w = csv.DictWriter(f, fieldnames=FUT_DAILY_FIELDS) - w.writeheader() - w.writerows(rows) - print(f" {csv_name:12s} {len(rows)} 条 ({rows[0]['trade_date']} ~ {rows[-1]['trade_date']})") - time.sleep(0.3) - - -# ── 数据库 ──────────────────────────────────── - -def init_db(): - conn = sqlite3.connect(DB_PATH) - conn.execute(""" - CREATE TABLE IF NOT EXISTS daily ( - ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, - pre_close REAL, pre_settle REAL, - open REAL, high REAL, low REAL, close REAL, settle REAL, - change1 REAL, change2 REAL, - vol REAL, amount REAL, oi REAL, oi_chg REAL, - delv_settle REAL, - PRIMARY KEY (ts_code, trade_date) - ) - """) - conn.execute(""" - CREATE TABLE IF NOT EXISTS contracts ( - ts_code TEXT PRIMARY KEY, - symbol TEXT, name TEXT, exchange TEXT, - fut_code TEXT, list_date TEXT, delist_date TEXT - ) - """) - conn.execute(""" - CREATE TABLE IF NOT EXISTS spread ( - trade_date TEXT PRIMARY KEY, - spread REAL, - main_ts_code TEXT, - main_chg REAL - ) - """) - try: - conn.execute("ALTER TABLE spread ADD COLUMN main_chg REAL") - except sqlite3.OperationalError: - pass - conn.commit() - conn.close() - - -def sync_contracts_to_db(fut_code, exchange, contracts): - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - for c in contracts: - cursor.execute( - "INSERT OR REPLACE INTO contracts VALUES (?, ?, ?, ?, ?, ?, ?)", - (c["ts_code"], c["symbol"], c["name"], exchange, fut_code, - c["list_date"], c.get("delist_date")), - ) - conn.commit() - conn.close() - print(f" contracts 表同步 {len(contracts)} 条") - - -def sync_rows_to_db(rows): - if not rows: - return - conn = sqlite3.connect(DB_PATH) - placeholders = ",".join("?" for _ in FUT_DAILY_FIELDS) - cols = ",".join(FUT_DAILY_FIELDS) - sql = f"INSERT OR REPLACE INTO daily ({cols}) VALUES ({placeholders})" - vals = [[r.get(c) for c in FUT_DAILY_FIELDS] for r in rows] - conn.executemany(sql, vals) - conn.commit() - conn.close() - print(f" daily 表更新 {len(rows)} 条") - - -def sync_all_csv_to_db(): - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - for root, _, files in os.walk(DATA_DIR): - for fname in sorted(files): - if not fname.endswith(".csv"): - continue - fpath = os.path.join(root, fname) - with open(fpath) as f: - reader = csv.DictReader(f) - rows = list(reader) - if not rows: - continue - placeholders = ",".join("?" for _ in FUT_DAILY_FIELDS) - cols = ",".join(FUT_DAILY_FIELDS) - sql = f"INSERT OR REPLACE INTO daily ({cols}) VALUES ({placeholders})" - vals = [[r.get(c) for c in FUT_DAILY_FIELDS] for r in rows] - cursor.executemany(sql, vals) - print(f" {fname:12s} {len(rows)} 条") - conn.commit() - conn.close() - - -def sync_spread(): - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - dates = conn.execute("SELECT DISTINCT trade_date FROM daily ORDER BY trade_date").fetchall() - - count = 0 - for d in dates: - date_str = d["trade_date"] - rows = conn.execute(""" - SELECT d.ts_code, d.close, d.open, d.vol - 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 c.delist_date - """, (date_str, date_str)).fetchall() - if len(rows) < 2: - continue - - closes = [] - main_ts = None - main_vol = -1 - main_chg = None - for r in rows: - try: - closes.append(float(r["close"])) - except (TypeError, ValueError): - closes.append(None) - if r["ts_code"][4:6] in ("01", "05", "09"): - v = r["vol"] - if v is not None and v > main_vol: - main_vol = v - main_ts = r["ts_code"] - try: - c = float(r["close"]) - o = float(r["open"]) - main_chg = c - o - except (TypeError, ValueError): - main_chg = None - - closes = [c for c in closes if c is not None] - if len(closes) < 2: - continue - - spread = closes[-1] - closes[0] - conn.execute( - "INSERT OR REPLACE INTO spread VALUES (?, ?, ?, ?)", - (date_str, spread, main_ts, main_chg), - ) - count += 1 - - conn.commit() - conn.close() - print(f" spread 表更新 {count} 个交易日") - - -# ── 主入口 ──────────────────────────────────── - -FG_EXTRA_CODES = {f"FG{suffix}.ZCE" for suffix in - [f"14{i:02d}" for i in range(1, 13)] + - [f"15{i:02d}" for i in range(1, 13)] + - [f"16{i:02d}" for i in range(1, 13)] + - [f"17{i:02d}" for i in range(1, 13)] + - [f"18{i:02d}" for i in range(1, 13)] + - [f"19{i:02d}" for i in range(1, 13)] + - [f"20{i:02d}" for i in range(1, 13)] + - [f"21{i:02d}" for i in range(1, 13)] + - [f"22{i:02d}" for i in range(1, 13)] + - [f"23{i:02d}" for i in range(1, 13)] + - [f"24{i:02d}" for i in range(1, 13)] + - [f"25{i:02d}" for i in range(1, 13)] + - [f"26{i:02d}" for i in range(1, 8)]} - - -if __name__ == "__main__": - full_init = "--init" in sys.argv - init_db() - active = get_active_contracts("CZCE", "FG") - - if full_init: - all_fg = get_all_contracts("CZCE", "FG") - active_codes = {c["ts_code"] for c in active} - extra = [c for c in all_fg if c["ts_code"] in FG_EXTRA_CODES and c["ts_code"] not in active_codes] - contracts = active + extra - print(f"全量初始化: {len(contracts)} 个合约") - for c in contracts: - print(f" {c['ts_code'].split('.')[0]:8s} 上市:{c['list_date']} 退市:{c.get('delist_date','-')}") - fetch_all_csv("FG", contracts) - sync_contracts_to_db("FG", "CZCE", contracts) - sync_all_csv_to_db() - sync_spread() - else: - if not is_trading_day(): - print("今天非交易日,无需更新") - print("完成") - sys.exit(0) - print(f"增量更新: {len(active)} 个活跃合约") - new_rows = update_all_contracts("FG", active) - if new_rows: - sync_contracts_to_db("FG", "CZCE", active) - sync_rows_to_db(new_rows) - else: - print(" 无新数据") - - sync_spread() - print("完成")