"""Symbol conversion between internal A-share ids and JoinQuant ids.""" from __future__ import annotations import re _INTERNAL_RE = re.compile(r"^(?Psh|sz)(?P\d{6})$", re.IGNORECASE) _JOINQUANT_RE = re.compile( r"^(?P\d{6})\.(?PXSHG|XSHE)$", re.IGNORECASE ) _BARE_RE = re.compile(r"^\d{6}$") def _validate_internal(prefix: str, code: str) -> None: if prefix == "sh" and code.startswith("6"): return if prefix == "sz" and code.startswith(("0", "3")): return raise ValueError(f"Unsupported A-share symbol: {prefix}{code}") def _validate_joinquant(code: str, exchange: str) -> None: if exchange == "XSHG" and code.startswith("6"): return if exchange == "XSHE" and code.startswith(("0", "3")): return raise ValueError(f"Unsupported JoinQuant A-share symbol: {code}.{exchange}") def to_joinquant_symbol(symbol_id: str) -> str: """Convert an internal symbol like ``sh600000`` to ``600000.XSHG``. Args: symbol_id: Internal A-share id with ``sh`` or ``sz`` prefix. Returns: JoinQuant security id. Raises: ValueError: If the symbol is malformed or outside supported A-share Shanghai/Shenzhen equity prefixes. """ text = str(symbol_id).strip().lower() match = _INTERNAL_RE.match(text) if not match: raise ValueError(f"Invalid internal symbol: {symbol_id!r}") prefix = match.group("prefix").lower() code = match.group("code") _validate_internal(prefix, code) exchange = "XSHG" if prefix == "sh" else "XSHE" return f"{code}.{exchange}" def from_joinquant_symbol(jq_symbol: str) -> str: """Convert a JoinQuant symbol like ``600000.XSHG`` to ``sh600000``.""" text = str(jq_symbol).strip().upper() match = _JOINQUANT_RE.match(text) if not match: raise ValueError(f"Invalid JoinQuant symbol: {jq_symbol!r}") code = match.group("code") exchange = match.group("exchange").upper() _validate_joinquant(code, exchange) prefix = "sh" if exchange == "XSHG" else "sz" return f"{prefix}{code}" def normalize_symbol_pair(value: object) -> tuple[str, str]: """Return ``(symbol_id, jq_symbol)`` from any supported symbol spelling.""" text = str(value).strip() if not text or text.lower() == "nan": raise ValueError("Missing symbol") if _INTERNAL_RE.match(text): symbol_id = text.lower() return symbol_id, to_joinquant_symbol(symbol_id) if _JOINQUANT_RE.match(text): jq_symbol = text.upper() return from_joinquant_symbol(jq_symbol), jq_symbol if _BARE_RE.match(text): if text.startswith("6"): symbol_id = f"sh{text}" elif text.startswith(("0", "3")): symbol_id = f"sz{text}" else: raise ValueError(f"Unsupported bare A-share code: {text}") return symbol_id, to_joinquant_symbol(symbol_id) raise ValueError(f"Unsupported symbol: {value!r}")