bd7f0da1ca
Co-authored-by: Cursor <cursoragent@cursor.com>
271 lines
11 KiB
Python
271 lines
11 KiB
Python
"""
|
|
期货品种与同花顺代码映射。
|
|
界面展示同花顺格式(如 ag2606、SR609、IF2606),行情通过新浪 API(内部 sina_code)获取。
|
|
"""
|
|
import re
|
|
import time
|
|
from datetime import date
|
|
from typing import Optional
|
|
|
|
import requests
|
|
|
|
# 品种字母:ths=同花顺展示用,sina=新浪 nf_ 前缀后字母(通常大写)
|
|
PRODUCTS = [
|
|
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "黄金", "ths": "au", "sina": "AU", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "铜", "ths": "cu", "sina": "CU", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "铝", "ths": "al", "sina": "AL", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "锌", "ths": "zn", "sina": "ZN", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "铅", "ths": "pb", "sina": "PB", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "镍", "ths": "ni", "sina": "NI", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "锡", "ths": "sn", "sina": "SN", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "螺纹钢", "ths": "rb", "sina": "RB", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "热卷", "ths": "hc", "sina": "HC", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "不锈钢", "ths": "ss", "sina": "SS", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "原油", "ths": "sc", "sina": "SC", "exchange": "上期能源", "ex": "INE"},
|
|
{"name": "燃油", "ths": "fu", "sina": "FU", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "沥青", "ths": "bu", "sina": "BU", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "橡胶", "ths": "ru", "sina": "RU", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "纸浆", "ths": "sp", "sina": "SP", "exchange": "上期所", "ex": "SHFE"},
|
|
{"name": "铁矿石", "ths": "i", "sina": "I", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "焦炭", "ths": "j", "sina": "J", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "焦煤", "ths": "jm", "sina": "JM", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "豆粕", "ths": "m", "sina": "M", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "豆油", "ths": "y", "sina": "Y", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "棕榈油", "ths": "p", "sina": "P", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "玉米", "ths": "c", "sina": "C", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "淀粉", "ths": "cs", "sina": "CS", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "鸡蛋", "ths": "jd", "sina": "JD", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "生猪", "ths": "lh", "sina": "LH", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "聚乙烯", "ths": "l", "sina": "L", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "聚丙烯", "ths": "pp", "sina": "PP", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "PVC", "ths": "v", "sina": "V", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "乙二醇", "ths": "eg", "sina": "EG", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "苯乙烯", "ths": "eb", "sina": "EB", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "液化气", "ths": "pg", "sina": "PG", "exchange": "大商所", "ex": "DCE"},
|
|
{"name": "菜粕", "ths": "RM", "sina": "RM", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "菜油", "ths": "OI", "sina": "OI", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "白糖", "ths": "SR", "sina": "SR", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "棉花", "ths": "CF", "sina": "CF", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "甲醇", "ths": "MA", "sina": "MA", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "PTA", "ths": "TA", "sina": "TA", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "玻璃", "ths": "FG", "sina": "FG", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "纯碱", "ths": "SA", "sina": "SA", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "尿素", "ths": "UR", "sina": "UR", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "硅铁", "ths": "SF", "sina": "SF", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "锰硅", "ths": "SM", "sina": "SM", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "苹果", "ths": "AP", "sina": "AP", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "红枣", "ths": "CJ", "sina": "CJ", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "花生", "ths": "PK", "sina": "PK", "exchange": "郑商所", "ex": "CZCE"},
|
|
{"name": "沪深300", "ths": "IF", "sina": "IF", "exchange": "中金所", "ex": "CFFEX"},
|
|
{"name": "上证50", "ths": "IH", "sina": "IH", "exchange": "中金所", "ex": "CFFEX"},
|
|
{"name": "中证500", "ths": "IC", "sina": "IC", "exchange": "中金所", "ex": "CFFEX"},
|
|
{"name": "中证1000", "ths": "IM", "sina": "IM", "exchange": "中金所", "ex": "CFFEX"},
|
|
]
|
|
|
|
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
|
|
_CACHE_TTL = 300 # 5 分钟
|
|
|
|
|
|
def _sina_headers() -> dict:
|
|
return {"Referer": "https://finance.sina.com.cn"}
|
|
|
|
|
|
def _fetch_sina_raw(sina_code: str) -> Optional[dict]:
|
|
try:
|
|
url = f"https://hq.sinajs.cn/list={sina_code}"
|
|
resp = requests.get(url, headers=_sina_headers(), timeout=5)
|
|
resp.encoding = "gbk"
|
|
if '"' not in resp.text:
|
|
return None
|
|
body = resp.text.split('"')[1]
|
|
if not body:
|
|
return None
|
|
parts = body.split(",")
|
|
if len(parts) < 9:
|
|
return None
|
|
price = float(parts[8])
|
|
volume = float(parts[14]) if len(parts) > 14 and parts[14] else 0
|
|
return {"name": parts[0], "price": price, "volume": volume, "parts": parts}
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def build_ths_code(product: dict, year: int, month: int) -> str:
|
|
"""同花顺合约代码。"""
|
|
ex = product["ex"]
|
|
letters = product["ths"]
|
|
if ex == "CZCE":
|
|
return f"{letters}{year % 10}{month:02d}"
|
|
return f"{letters}{year % 100:02d}{month:02d}"
|
|
|
|
|
|
def build_sina_code(product: dict, year: int, month: int) -> str:
|
|
"""新浪行情代码(用于拉价)。"""
|
|
ex = product["ex"]
|
|
letters = product["sina"]
|
|
suffix = f"{year % 100:02d}{month:02d}"
|
|
if ex == "CFFEX":
|
|
return f"CFF_RE_{letters}{suffix}"
|
|
return f"nf_{letters}{suffix}"
|
|
|
|
|
|
def build_sina_main_code(product: dict) -> str:
|
|
"""新浪主力连续代码。"""
|
|
ex = product["ex"]
|
|
letters = product["sina"]
|
|
if ex == "CFFEX":
|
|
return f"CFF_RE_{letters}0"
|
|
return f"nf_{letters}0"
|
|
|
|
|
|
def ths_to_sina_code(ths_code: str) -> Optional[str]:
|
|
"""将同花顺代码转为新浪代码(用户直接输入合约时使用)。"""
|
|
code = ths_code.strip()
|
|
if not code:
|
|
return None
|
|
|
|
# CFFEX / 四位月份: IF2606, ag2606
|
|
m = re.match(r"^([A-Za-z]+)(\d{4})$", code)
|
|
if m:
|
|
letters, digits = m.group(1), m.group(2)
|
|
letters_up = letters.upper()
|
|
for p in PRODUCTS:
|
|
if p["ths"].upper() == letters_up or p["sina"] == letters_up:
|
|
year = 2000 + int(digits[:2])
|
|
month = int(digits[2:])
|
|
if 1 <= month <= 12:
|
|
return build_sina_code(p, year, month)
|
|
if letters_up in ("IF", "IH", "IC", "IM", "T", "TF", "TS"):
|
|
return f"CFF_RE_{letters_up}{digits}"
|
|
|
|
# CZCE 3-digit: SR609
|
|
m3 = re.match(r"^([A-Za-z]+)(\d{3})$", code)
|
|
if m3:
|
|
letters, digits = m3.group(1), m3.group(2)
|
|
letters_up = letters.upper()
|
|
y_digit = int(digits[0])
|
|
month = int(digits[1:])
|
|
if 1 <= month <= 12:
|
|
year = date.today().year
|
|
decade = year // 10 * 10
|
|
candidate = decade + y_digit
|
|
if candidate < year - 1:
|
|
candidate += 10
|
|
for p in PRODUCTS:
|
|
if p["ths"].upper() == letters_up or p["sina"] == letters_up:
|
|
return build_sina_code(p, candidate, month)
|
|
|
|
return None
|
|
|
|
|
|
def resolve_main_contract(product: dict) -> Optional[dict]:
|
|
"""按成交量选取当前主力月份合约。"""
|
|
cache_key = product["sina"]
|
|
now = time.time()
|
|
cached = _MAIN_CACHE.get(cache_key)
|
|
if cached and now - cached[0] < _CACHE_TTL:
|
|
return cached[1]
|
|
|
|
today = date.today()
|
|
y, m = today.year, today.month
|
|
best = None
|
|
|
|
for i in range(14):
|
|
cy, cm = y, m + i
|
|
while cm > 12:
|
|
cm -= 12
|
|
cy += 1
|
|
sina = build_sina_code(product, cy, cm)
|
|
raw = _fetch_sina_raw(sina)
|
|
if raw and raw["volume"] > 0:
|
|
ths = build_ths_code(product, cy, cm)
|
|
item = {
|
|
"name": product["name"],
|
|
"ths_code": ths,
|
|
"sina_code": sina,
|
|
"exchange": product["exchange"],
|
|
"contract": f"主力 {ths}",
|
|
"display": f"{product['name']} 主力 {ths}",
|
|
"volume": raw["volume"],
|
|
}
|
|
if best is None or raw["volume"] > best["volume"]:
|
|
best = item
|
|
|
|
if best is None:
|
|
sina_main = build_sina_main_code(product)
|
|
raw = _fetch_sina_raw(sina_main)
|
|
if raw:
|
|
ths_letters = product["ths"]
|
|
ths_main = f"{ths_letters}888" if product["ex"] != "CFFEX" else f"{ths_letters.upper()}888"
|
|
best = {
|
|
"name": product["name"],
|
|
"ths_code": ths_main,
|
|
"sina_code": sina_main,
|
|
"exchange": product["exchange"],
|
|
"contract": f"主力连续 {ths_main}",
|
|
"display": f"{product['name']} 主力连续 {ths_main}",
|
|
"volume": raw.get("volume", 0),
|
|
}
|
|
|
|
if best:
|
|
_MAIN_CACHE[cache_key] = (now, best)
|
|
return best
|
|
|
|
|
|
def search_symbols(query: str) -> list:
|
|
q = query.strip().lower()
|
|
if not q:
|
|
return []
|
|
|
|
results = []
|
|
for p in PRODUCTS:
|
|
name = p["name"]
|
|
if q not in name.lower() and q not in p["ths"].lower() and q not in p["sina"].lower():
|
|
continue
|
|
main = resolve_main_contract(p)
|
|
if main:
|
|
results.append(main)
|
|
|
|
# 用户直接输入同花顺合约代码
|
|
if not results and len(q) >= 3:
|
|
sina = ths_to_sina_code(query.strip())
|
|
if sina:
|
|
raw = _fetch_sina_raw(sina)
|
|
if raw:
|
|
results.append({
|
|
"name": raw["name"],
|
|
"ths_code": query.strip(),
|
|
"sina_code": sina,
|
|
"exchange": "",
|
|
"contract": query.strip(),
|
|
"display": f"{raw['name']} ({query.strip()})",
|
|
"volume": raw.get("volume", 0),
|
|
})
|
|
|
|
return results[:12]
|
|
|
|
|
|
def get_price(sina_code: str) -> Optional[float]:
|
|
raw = _fetch_sina_raw(sina_code)
|
|
return raw["price"] if raw else None
|
|
|
|
|
|
def get_by_ths_code(ths_code: str) -> Optional[dict]:
|
|
for p in PRODUCTS:
|
|
main = resolve_main_contract(p)
|
|
if main and main["ths_code"].lower() == ths_code.lower():
|
|
return main
|
|
sina = ths_to_sina_code(ths_code)
|
|
if sina:
|
|
raw = _fetch_sina_raw(sina)
|
|
if raw:
|
|
return {
|
|
"name": raw["name"],
|
|
"ths_code": ths_code,
|
|
"sina_code": sina,
|
|
"exchange": "",
|
|
"contract": ths_code,
|
|
}
|
|
return None
|