部署改回/opt;接入同花顺iFinD HTTP行情,新浪作回退
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+102
-109
@@ -1,15 +1,14 @@
|
||||
"""
|
||||
期货品种与同花顺代码映射。
|
||||
界面展示同花顺格式(如 ag2606、SR609、IF2606),行情通过新浪 API(内部 sina_code)获取。
|
||||
展示同花顺合约代码(ag2608);行情优先走同花顺 iFinD HTTP,回退新浪。
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
|
||||
|
||||
# 品种字母:ths=同花顺展示用,sina=新浪 nf_ 前缀后字母(通常大写)
|
||||
PRODUCTS = [
|
||||
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
||||
{"name": "黄金", "ths": "au", "sina": "AU", "exchange": "上期所", "ex": "SHFE"},
|
||||
@@ -64,35 +63,11 @@ PRODUCTS = [
|
||||
]
|
||||
|
||||
_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
|
||||
_CACHE_TTL = 300
|
||||
|
||||
|
||||
def build_ths_code(product: dict, year: int, month: int) -> str:
|
||||
"""同花顺合约代码。"""
|
||||
"""同花顺软件内显示的合约代码。"""
|
||||
ex = product["ex"]
|
||||
letters = product["ths"]
|
||||
if ex == "CZCE":
|
||||
@@ -100,67 +75,108 @@ def build_ths_code(product: dict, year: int, month: int) -> str:
|
||||
return f"{letters}{year % 100:02d}{month:02d}"
|
||||
|
||||
|
||||
def build_ths_full_code(product: dict, year: int, month: int) -> str:
|
||||
"""同花顺 iFinD HTTP API 代码,如 ag2608.SHFE"""
|
||||
ths = build_ths_code(product, year, month)
|
||||
suffix = THS_EX_SUFFIX.get(product["ex"], product["ex"])
|
||||
return f"{ths}.{suffix}"
|
||||
|
||||
|
||||
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":
|
||||
if product["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":
|
||||
if product["ex"] == "CFFEX":
|
||||
return f"CFF_RE_{letters}0"
|
||||
return f"nf_{letters}0"
|
||||
|
||||
|
||||
def ths_to_sina_code(ths_code: str) -> Optional[str]:
|
||||
"""将同花顺代码转为新浪代码(用户直接输入合约时使用)。"""
|
||||
def _find_product_by_letters(letters: str) -> Optional[dict]:
|
||||
letters_up = letters.upper()
|
||||
for p in PRODUCTS:
|
||||
if p["ths"].upper() == letters_up or p["sina"] == letters_up:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def ths_to_codes(ths_code: str) -> Optional[dict]:
|
||||
"""同花顺合约代码 -> ths_full + sina 回退代码。"""
|
||||
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)
|
||||
m4 = re.match(r"^([A-Za-z]+)(\d{4})$", code)
|
||||
if m4:
|
||||
letters, digits = m4.group(1), m4.group(2)
|
||||
year = 2000 + int(digits[:2])
|
||||
month = int(digits[2:])
|
||||
if not 1 <= month <= 12:
|
||||
return None
|
||||
product = _find_product_by_letters(letters)
|
||||
if product:
|
||||
return {
|
||||
"ths_code": build_ths_code(product, year, month),
|
||||
"market_code": build_ths_full_code(product, year, month),
|
||||
"sina_code": build_sina_code(product, year, month),
|
||||
}
|
||||
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}"
|
||||
ths = f"{letters_up}{digits}"
|
||||
return {
|
||||
"ths_code": ths,
|
||||
"market_code": f"{ths}.CFFEX",
|
||||
"sina_code": 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)
|
||||
if not 1 <= month <= 12:
|
||||
return None
|
||||
year = date.today().year
|
||||
decade = year // 10 * 10
|
||||
candidate = decade + y_digit
|
||||
if candidate < year - 1:
|
||||
candidate += 10
|
||||
product = _find_product_by_letters(letters)
|
||||
if product:
|
||||
return {
|
||||
"ths_code": build_ths_code(product, candidate, month),
|
||||
"market_code": build_ths_full_code(product, candidate, month),
|
||||
"sina_code": build_sina_code(product, candidate, month),
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def ths_to_sina_code(ths_code: str) -> Optional[str]:
|
||||
codes = ths_to_codes(ths_code)
|
||||
return codes["sina_code"] if codes else None
|
||||
|
||||
|
||||
def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> dict:
|
||||
ths = build_ths_code(product, year, month)
|
||||
return {
|
||||
"name": product["name"],
|
||||
"ths_code": ths,
|
||||
"market_code": build_ths_full_code(product, year, month),
|
||||
"sina_code": build_sina_code(product, year, month),
|
||||
"exchange": product["exchange"],
|
||||
"contract": f"主力 {ths}",
|
||||
"display": f"{product['name']} 主力 {ths}",
|
||||
"volume": volume,
|
||||
}
|
||||
|
||||
|
||||
def resolve_main_contract(product: dict) -> Optional[dict]:
|
||||
"""按成交量选取当前主力月份合约。"""
|
||||
cache_key = product["sina"]
|
||||
now = time.time()
|
||||
cached = _MAIN_CACHE.get(cache_key)
|
||||
@@ -177,30 +193,27 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
||||
cm -= 12
|
||||
cy += 1
|
||||
sina = build_sina_code(product, cy, cm)
|
||||
raw = _fetch_sina_raw(sina)
|
||||
raw = fetch_raw_for_volume(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"],
|
||||
}
|
||||
item = _make_symbol_item(product, cy, cm, 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)
|
||||
raw = fetch_raw_for_volume(sina_main)
|
||||
if raw:
|
||||
ths_letters = product["ths"]
|
||||
ths_main = f"{ths_letters}888" if product["ex"] != "CFFEX" else f"{ths_letters.upper()}888"
|
||||
ths_main = (
|
||||
f"{ths_letters}888"
|
||||
if product["ex"] != "CFFEX"
|
||||
else f"{ths_letters.upper()}888"
|
||||
)
|
||||
suffix = THS_EX_SUFFIX.get(product["ex"], product["ex"])
|
||||
best = {
|
||||
"name": product["name"],
|
||||
"ths_code": ths_main,
|
||||
"market_code": f"{ths_main}.{suffix}",
|
||||
"sina_code": sina_main,
|
||||
"exchange": product["exchange"],
|
||||
"contract": f"主力连续 {ths_main}",
|
||||
@@ -227,44 +240,24 @@ def search_symbols(query: str) -> list:
|
||||
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),
|
||||
})
|
||||
codes = ths_to_codes(query.strip())
|
||||
if codes:
|
||||
raw = fetch_raw_for_volume(codes["sina_code"])
|
||||
name = raw["name"] if raw else query.strip()
|
||||
results.append({
|
||||
"name": name,
|
||||
"ths_code": codes["ths_code"],
|
||||
"market_code": codes["market_code"],
|
||||
"sina_code": codes["sina_code"],
|
||||
"exchange": "",
|
||||
"contract": codes["ths_code"],
|
||||
"display": f"{name} ({codes['ths_code']})",
|
||||
"volume": raw.get("volume", 0) if raw else 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
|
||||
def get_price(market_code: str, sina_code: str = "") -> Optional[float]:
|
||||
return market_get_price(market_code, sina_code)
|
||||
|
||||
Reference in New Issue
Block a user