清理所有 Python 脚本和 HTML 图表文件
This commit is contained in:
@@ -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
File diff suppressed because one or more lines are too long
@@ -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>玻璃期货 — 合约链差价 & 主力合约日内涨跌</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()
|
||||
@@ -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 查看当月方向")
|
||||
@@ -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()
|
||||
File diff suppressed because one or more lines are too long
-402
@@ -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> σ: <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)
|
||||
@@ -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()
|
||||
@@ -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("完成")
|
||||
Reference in New Issue
Block a user