diff --git a/chart.html b/chart.html
new file mode 100644
index 0000000..4cb0bc7
--- /dev/null
+++ b/chart.html
@@ -0,0 +1,121 @@
+
+
+
+
+
+玻璃期货 spread 走势
+
+
+
+
+玻璃期货 — 合约链差价 & 主力合约日内涨跌
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chart.py b/chart.py
new file mode 100644
index 0000000..62d4cd1
--- /dev/null
+++ b/chart.py
@@ -0,0 +1,149 @@
+"""
+生成 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/quote.py b/quote.py
new file mode 100644
index 0000000..1e28ac4
--- /dev/null
+++ b/quote.py
@@ -0,0 +1,87 @@
+"""
+查询指定交易日玻璃期货主力合约行情。
+用法: 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/update.py b/update.py
index d150b6c..1902089 100644
--- a/update.py
+++ b/update.py
@@ -20,40 +20,22 @@ FUT_DAILY_FIELDS = [
]
-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 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",
@@ -62,7 +44,7 @@ def is_trading_day(exchange="CZCE"):
)
if rows:
return rows[0]["is_open"] == "1"
- return True # 查不到时保守放行
+ return True
# ── 合约列表 ──────────────────────────────────
@@ -88,18 +70,12 @@ def get_all_contracts(exchange, fut_code):
# ── 增量更新 CSV ──────────────────────────────
def update_contract_csv(fut_code, contract):
- """
- 增量更新单份合约 CSV。
- 从已有数据的最后交易日重新拉取(含该日),实现补漏 + 修正。
- 返回新增/修改的行,或 None(无变化)。
- """
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:
@@ -108,7 +84,6 @@ def update_contract_csv(fut_code, contract):
last_date = existing[-1]["trade_date"] if existing else None
- # 从最后交易日开始拉取(包含该日,以便修正)
params = {"ts_code": ts_code}
if last_date:
params["start_date"] = last_date
@@ -120,7 +95,6 @@ def update_contract_csv(fut_code, contract):
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()
@@ -128,14 +102,10 @@ def update_contract_csv(fut_code, contract):
return rows
if rows[0]["trade_date"] == last_date:
- # 最后交易日可能有修正 → 替换最后一行再追加
existing = existing[:-1]
merged = existing + rows
- elif rows[0]["trade_date"] > last_date:
- # 纯新增数据
- merged = existing + rows
else:
- merged = existing + rows # 安全兜底
+ merged = existing + rows
with open(csv_path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=FUT_DAILY_FIELDS)
@@ -146,7 +116,6 @@ def update_contract_csv(fut_code, contract):
def update_all_contracts(fut_code, contracts):
- """批量增量更新,返回所有有变化的行"""
all_new = []
for c in contracts:
code = c["ts_code"].split(".")[0]
@@ -165,27 +134,21 @@ def update_all_contracts(fut_code, contracts):
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),
+ "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)
@@ -196,9 +159,8 @@ 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,
+ 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,
@@ -213,6 +175,18 @@ def init_db():
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()
@@ -232,7 +206,6 @@ def sync_contracts_to_db(fut_code, exchange, contracts):
def sync_rows_to_db(rows):
- """只同步有变化的行到 daily 表(增量更新用)"""
if not rows:
return
conn = sqlite3.connect(DB_PATH)
@@ -247,7 +220,6 @@ def sync_rows_to_db(rows):
def sync_all_csv_to_db():
- """全量同步所有 CSV 到 daily 表(初始化用)"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
for root, _, files in os.walk(DATA_DIR):
@@ -270,6 +242,63 @@ def sync_all_csv_to_db():
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
@@ -290,7 +319,6 @@ FG_EXTRA_CODES = {f"FG{suffix}.ZCE" for suffix in
if __name__ == "__main__":
full_init = "--init" in sys.argv
-
init_db()
active = get_active_contracts("CZCE", "FG")
@@ -301,17 +329,16 @@ if __name__ == "__main__":
contracts = active + extra
print(f"全量初始化: {len(contracts)} 个合约")
for c in contracts:
- code = c["ts_code"].split(".")[0]
- print(f" {code:8s} 上市:{c['list_date']} 退市:{c.get('delist_date','-')}")
+ 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:
@@ -320,4 +347,5 @@ if __name__ == "__main__":
else:
print(" 无新数据")
+ sync_spread()
print("完成")