""" 期货品种与同花顺代码映射。 展示同花顺合约代码(ag2608);行情默认新浪,机构用户可通过环境变量启用同花顺 iFinD。 """ import re import time from datetime import date from typing import Optional from market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX 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 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_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: letters = product["sina"] suffix = f"{year % 100:02d}{month:02d}" if product["ex"] == "CFFEX": return f"CFF_RE_{letters}{suffix}" return f"nf_{letters}{suffix}" def build_sina_main_code(product: dict) -> str: letters = product["sina"] if product["ex"] == "CFFEX": return f"CFF_RE_{letters}0" return f"nf_{letters}0" 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 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() if letters_up in ("IF", "IH", "IC", "IM", "T", "TF", "TS"): ths = f"{letters_up}{digits}" return { "ths_code": ths, "market_code": f"{ths}.CFFEX", "sina_code": f"CFF_RE_{letters_up}{digits}", } m3 = re.match(r"^([A-Za-z]+)(\d{3})$", code) if m3: letters, digits = m3.group(1), m3.group(2) y_digit = int(digits[0]) month = int(digits[1:]) 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) 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_raw_for_volume(sina) if raw and raw["volume"] > 0: 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_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" ) 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}", "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: 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(market_code: str, sina_code: str = "") -> Optional[float]: return market_get_price(market_code, sina_code)