清理所有 Python 脚本和 HTML 图表文件

This commit is contained in:
fish
2026-07-20 09:54:58 +08:00
parent d74829ad32
commit b3c46a512f
9 changed files with 0 additions and 1645 deletions
-226
View File
@@ -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)")
-121
View File
File diff suppressed because one or more lines are too long
-149
View File
@@ -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"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>玻璃期货 spread 走势</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ background: #0f172a; color: #e2e8f0; font-family: system-ui, sans-serif; padding: 24px; }}
h1 {{ font-size: 20px; margin-bottom: 24px; color: #f1f5f9; }}
.chart-box {{ background: #1e293b; border-radius: 12px; padding: 20px; }}
canvas {{ width: 100% !important; height: auto !important; }}
.stats {{ display: flex; gap: 24px; flex-wrap: wrap; margin-top: 24px; }}
.stat {{ background: #1e293b; border-radius: 12px; padding: 16px 20px; min-width: 130px; }}
.stat-label {{ font-size: 12px; color: #94a3b8; }}
.stat-value {{ font-size: 22px; font-weight: 600; margin-top: 4px; }}
.stat-value.pos {{ color: #22c55e; }}
.stat-value.neg {{ color: #ef4444; }}
</style>
</head>
<body>
<h1>玻璃期货 — 合约链差价 &amp; 主力合约日内涨跌</h1>
<div class="chart-box">
<canvas id="chart"></canvas>
</div>
<div class="stats" id="stats"></div>
<script>
const dates = {json.dumps(dates)};
const spreads = {json.dumps(spreads)};
const chgs = {json.dumps(chgs)};
new Chart(document.getElementById('chart'), {{
type: 'bar',
data: {{
labels: dates,
datasets: [
{{
label: '主力日内涨跌',
data: chgs,
yAxisID: 'y1',
backgroundColor: chgs.map(v => v === null ? 'transparent' : v >= 0 ? 'rgba(34,197,94,0.35)' : 'rgba(239,68,68,0.35)'),
borderColor: chgs.map(v => v === null ? 'transparent' : v >= 0 ? '#22c55e' : '#ef4444'),
borderWidth: 0.3,
order: 2,
}},
{{
label: '差价合计',
data: spreads,
yAxisID: 'y',
type: 'line',
borderColor: '#facc15',
backgroundColor: 'rgba(250,204,21,0.06)',
borderWidth: 1.5,
pointRadius: 0,
fill: true,
tension: 0.1,
order: 1,
}},
]
}},
options: {{
responsive: true,
interaction: {{ mode: 'index', intersect: false }},
plugins: {{
legend: {{ labels: {{ color: '#94a3b8', boxWidth: 14, padding: 16 }} }},
tooltip: {{
backgroundColor: '#0f172a',
titleColor: '#f1f5f9',
bodyColor: '#e2e8f0',
borderColor: '#334155',
borderWidth: 1,
callbacks: {{
label: ctx => ctx.parsed.y !== null && ctx.parsed.y !== undefined
? `${{ctx.dataset.label}}: ${{ctx.parsed.y.toFixed(1)}}` : ''
}}
}}
}},
scales: {{
x: {{ ticks: {{ color: '#64748b', maxTicksLimit: 20, font: {{ size: 10 }} }}, grid: {{ color: '#1e293b' }} }},
y: {{
position: 'left',
ticks: {{ color: '#facc15' }},
grid: {{ color: '#334155' }},
title: {{ display: true, text: '差价合计(元/吨)', color: '#facc15' }}
}},
y1: {{
position: 'right',
ticks: {{ color: '#94a3b8' }},
grid: {{ drawOnChartArea: false }},
title: {{ display: true, text: '主力日内涨跌(元/吨)', color: '#94a3b8' }}
}}
}}
}}
}});
// 统计
const pos = spreads.filter(v => v > 0).length;
const neg = spreads.filter(v => v < 0).length;
const avg = spreads.reduce((a,b) => a+b, 0) / spreads.length;
const maxV = Math.max(...spreads);
const minV = Math.min(...spreads);
const totalChg = chgs.filter(v => v !== null).reduce((a,b) => a+b, 0);
const avgChg = totalChg / chgs.filter(v => v !== null).length;
const statsHtml = `
<div class="stat"><div class="stat-label">数据天数</div><div class="stat-value">${{spreads.length}}</div></div>
<div class="stat"><div class="stat-label">平均差价</div><div class="stat-value ${{avg>=0?'pos':'neg'}}">${{avg.toFixed(1)}}</div></div>
<div class="stat"><div class="stat-label">升水天数</div><div class="stat-value pos">${{pos}}</div></div>
<div class="stat"><div class="stat-label">贴水天数</div><div class="stat-value neg">${{neg}}</div></div>
<div class="stat"><div class="stat-label">最大升水</div><div class="stat-value pos">+${{maxV.toFixed(1)}}</div></div>
<div class="stat"><div class="stat-label">最大贴水</div><div class="stat-value neg">${{minV.toFixed(1)}}</div></div>
<div class="stat"><div class="stat-label">主力日均涨跌</div><div class="stat-value ${{avgChg>=0?'pos':'neg'}}">${{avgChg.toFixed(2)}}</div></div>
`;
document.getElementById('stats').innerHTML = statsHtml;
</script>
</body>
</html>"""
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()
-87
View File
@@ -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 查看当月方向")
-87
View File
@@ -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()
-163
View File
File diff suppressed because one or more lines are too long
-402
View File
@@ -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"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FG 主力合约价差走势</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0f172a; color: #e2e8f0; font-family: system-ui, sans-serif; padding: 24px; }
h1 { font-size: 20px; margin-bottom: 8px; color: #f1f5f9; }
.sub { font-size: 13px; color: #64748b; margin-bottom: 24px; }
.chart-box { background: #1e293b; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.chart-box canvas { width: 100% !important; }
.stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
.stat-card { background: #1e293b; border-radius: 12px; padding: 16px 20px; min-width: 170px; flex: 1; }
.stat-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
.stat-row { font-size: 12px; color: #94a3b8; margin: 2px 0; display: flex; justify-content: space-between; }
.stat-row .val { color: #e2e8f0; font-weight: 500; }
.val.pos { color: #22c55e; }
.val.neg { color: #ef4444; }
.sig-section { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
.sig-card { background: #1e293b; border-radius: 12px; padding: 16px 20px; min-width: 210px; flex: 1; border-left: 3px solid #334155; }
.sig-card.bull { border-left-color: #22c55e; }
.sig-card.bear { border-left-color: #ef4444; }
.sig-card.neutral { border-left-color: #64748b; }
.sig-hdr { font-size: 12px; color: #64748b; margin-bottom: 2px; }
.sig-action { font-size: 16px; font-weight: 700; margin-bottom: 8px; }
.sig-row { font-size: 12px; color: #94a3b8; margin: 2px 0; }
.sig-row b { color: #e2e8f0; }
</style>
</head>
<body>
<h1>玻璃期货 — 主力合约价差分析</h1>
<p class="sub">1月 / 5月 / 9月 两两价差(同年配对,远月减近月) | 均值回归信号(60日滚动,2σ 入场)</p>
<div class="chart-box">
<canvas id="chart"></canvas>
</div>
<div class="stats" id="stats"></div>
<div class="sig-section" id="signals"></div>
<div class="chart-box">
<canvas id="zchart"></canvas>
</div>
<script>
const D = __DATA__;
// ── 统计卡片 ──
let statsHtml = '';
for (const s of D.statCards) {
const cls = v => v >= 0 ? 'pos' : 'neg';
statsHtml += `
<div class="stat-card">
<div class="stat-title" style="color:${s.color}">${s.label}</div>
<div class="stat-row"><span>数据天数</span><span class="val">${s.count}</span></div>
<div class="stat-row"><span>平均价差</span><span class="val ${cls(s.avg)}">${s.avg}</span></div>
<div class="stat-row"><span>最大升水</span><span class="val pos">+${s.max}</span></div>
<div class="stat-row"><span>最大贴水</span><span class="val neg">${s.min}</span></div>
<div class="stat-row"><span>升水/贴水</span><span class="val"><span class="pos">${s.pos}</span>/<span class="neg">${s.neg}</span></span></div>
</div>`;
}
document.getElementById('stats').innerHTML = statsHtml;
// ── 信号卡片 ──
let sigHtml = '';
for (const s of D.sigCards) {
const cls = v => v >= 0 ? 'pos' : 'neg';
sigHtml += `
<div class="sig-card ${s.sigType}">
<div class="sig-hdr">${s.label}</div>
<div class="sig-action" style="color:${s.sigColor}">${s.action}</div>
<div class="sig-row">当前价差: <b class="${cls(s.latest)}">${s.latest}</b> (${s.latestDate})</div>
<div class="sig-row">近60日均值: <b>${s.mean}</b> &sigma;: <b>${s.std}</b></div>
<div class="sig-row">z值: <b class="${cls(s.zScore)}">${s.zScore >= 0 ? '+' : ''}${s.zScore}</b> (超 2 触发)</div>
</div>`;
}
document.getElementById('signals').innerHTML = sigHtml;
// ── 主图:价差走势 ──
new Chart(document.getElementById('chart'), {
type: 'line',
data: { labels: D.allDates, datasets: D.datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#94a3b8', boxWidth: 14, padding: 16, usePointStyle: true } },
tooltip: {
backgroundColor: '#0f172a', titleColor: '#f1f5f9', bodyColor: '#e2e8f0',
borderColor: '#334155', borderWidth: 1,
callbacks: {
label: ctx => ctx.parsed.y !== null ? ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1) : ''
}
}
},
scales: {
x: { ticks: { color: '#64748b', maxTicksLimit: 20, font: { size: 10 } }, grid: { color: '#1e293b' } },
y: {
ticks: { color: '#94a3b8' }, grid: { color: '#334155' },
title: { display: true, text: '价差(元/吨)', color: '#94a3b8' }
}
}
}
});
// ── 副图:z-score ──
new Chart(document.getElementById('zchart'), {
type: 'line',
data: { labels: D.allDates, datasets: D.zDatasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#94a3b8', boxWidth: 14, padding: 16, usePointStyle: true } },
tooltip: {
backgroundColor: '#0f172a', titleColor: '#f1f5f9', bodyColor: '#e2e8f0',
borderColor: '#334155', borderWidth: 1,
callbacks: {
label: ctx => ctx.parsed.y !== null ? ctx.dataset.label + ': z=' + ctx.parsed.y.toFixed(2) : ''
}
}
},
scales: {
x: { ticks: { color: '#64748b', maxTicksLimit: 20, font: { size: 10 } }, grid: { color: '#1e293b' } },
y: {
ticks: { color: '#94a3b8' }, grid: { color: '#334155' },
title: { display: true, text: 'z-score', color: '#94a3b8' }
}
}
},
plugins: [{
id: 'thresholdLines',
afterDraw(chart) {
const ys = chart.scales.y;
const ctx = chart.ctx;
[{ v: 2, c: '#ef4444', l: '+2σ' }, { v: -2, c: '#22c55e', l: '-2σ' }].forEach(t => {
const y = ys.getPixelForValue(t.v);
ctx.save();
ctx.beginPath();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = t.c;
ctx.lineWidth = 1;
ctx.moveTo(chart.chartArea.left, y);
ctx.lineTo(chart.chartArea.right, y);
ctx.stroke();
ctx.restore();
ctx.fillStyle = t.c;
ctx.font = '11px system-ui';
ctx.fillText(t.l, chart.chartArea.right - 36, y - 4);
});
}
}]
});
</script>
</body>
</html>"""
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)
-59
View File
@@ -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()
-351
View File
@@ -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("完成")