"""Conservative Taiwan stock subsector classification for hotspot analysis. ``twstock.codes.group`` remains the authoritative broad industry. This module adds a smaller, auditable layer only where a code override or a narrow company name rule is reliable. Unmatched stocks keep their original industry group. """ from __future__ import annotations from typing import Any TAXONOMY_SOURCE = "twstock.codes.group + conservative code/name rules" TAXONOMY_LEVEL = "industry > subsector" CODE_OVERRIDES: dict[str, str] = { # Semiconductor "2303": "晶圓代工", "2330": "晶圓代工", "5347": "晶圓代工", "6770": "晶圓代工", "2379": "IC 設計", "2454": "IC 設計", "3034": "IC 設計", "3443": "IC 設計", "3661": "IC 設計", "5274": "IC 設計", "6415": "IC 設計", "2329": "封裝測試", "2449": "封裝測試", "3711": "封裝測試", "6239": "封裝測試", "6257": "封裝測試", "8150": "封裝測試", "2408": "記憶體", "2344": "記憶體", "2337": "記憶體", "3260": "記憶體", "8299": "記憶體", # Computer and peripherals "2382": "AI 伺服器", "3231": "AI 伺服器", "6669": "AI 伺服器", "3706": "伺服器", "8210": "伺服器", "2324": "筆電與品牌電腦", "2353": "筆電與品牌電腦", "2356": "筆電與品牌電腦", "2357": "筆電與品牌電腦", "4938": "筆電與品牌電腦", # Electronic components "2313": "PCB", "2368": "PCB", "2383": "PCB", "3037": "PCB", "3044": "PCB", "4958": "PCB", "5439": "PCB", "6191": "PCB", "6274": "PCB", "8046": "PCB", "2327": "被動元件", "2492": "被動元件", "3026": "被動元件", "6173": "被動元件", "6449": "被動元件", "2059": "機構件與散熱", "3017": "機構件與散熱", "3653": "機構件與散熱", # Optoelectronics "2409": "面板", "3481": "面板", "6116": "面板", "3008": "光學鏡頭", "3406": "光學鏡頭", "8069": "電子紙", # Communications "2412": "電信營運", "3045": "電信營運", "4904": "電信營運", "2345": "網通設備", "3596": "網通設備", "5388": "網通設備", "6285": "網通設備", # Shipping and transportation "2603": "貨櫃航運", "2609": "貨櫃航運", "2615": "貨櫃航運", "2605": "散裝航運", "2606": "散裝航運", "2612": "散裝航運", "2637": "散裝航運", "5608": "散裝航運", "2610": "航空", "2618": "航空", "2646": "航空", "6757": "航空", # Finance "2855": "證券期貨", "5864": "證券期貨", "6005": "證券期貨", "6015": "證券期貨", "6016": "證券期貨", "6020": "證券期貨", "6021": "證券期貨", "6023": "證券期貨", "6024": "證券期貨", "6026": "證券期貨", } NAME_RULES_BY_GROUP: dict[str, tuple[tuple[str, tuple[str, ...]], ...]] = { "金融保險業": ( ("證券期貨", ("證", "期")), ("保險", ("保", "產", "壽")), ("銀行", ("銀", "銀行", "商銀")), ), "航運業": ( ("航空", ("航空", "航太", "華航", "亞航", "虎航")), ("物流運輸", ("宅配", "貨運", "高鐵", "大榮", "中菲行", "捷迅")), ("港埠倉儲", ("港", "櫃", "榮運")), ("造船", ("船", "漢翔", "龍德")), ), "光電業": ( ("光學鏡頭", ("光學", "大立光", "玉晶光", "今國光", "佳能")), ("太陽能", ("太陽", "元晶", "茂迪", "聯合再生", "國碩", "安集")), ("LED", ("LED", "億光", "佰鴻", "宏齊", "光鋐", "富采")), ), "通信網路業": ( ("電信營運", ("電信", "台灣大", "遠傳", "中華電")), ("網通設備", ("網通", "智邦", "中磊", "啟碁", "神準", "合勤", "正文", "明泰")), ("光通訊", ("光通", "聯亞", "上詮", "華星光", "光聖", "波若威")), ), "生技醫療業": ( ("醫療器材", ("醫材", "醫療", "醫", "眼", "視陽", "晶碩", "精華")), ("藥品與新藥", ("藥", "生達", "美時", "保瑞", "神隆", "東洋")), ("生技研發", ("生技", "基因", "疫苗", "細胞")), ), } def classify_subsector(code: Any, name: Any, group: Any) -> dict[str, Any]: """Return a conservative fine-grained classification with audit metadata.""" normalized_code = str(code or "").replace(".TW", "").replace(".TWO", "").strip() normalized_name = str(name or "").strip() normalized_group = str(group or "").strip() or "未分類" label = CODE_OVERRIDES.get(normalized_code) source = "code_override" if label else "" if not label: for candidate, keywords in NAME_RULES_BY_GROUP.get(normalized_group, ()): if any(keyword and keyword in normalized_name for keyword in keywords): label = candidate source = "name_keyword" break if not label: return { "subsector": normalized_group, "subsector_path": normalized_group, "subsector_source": "industry_fallback", "subsector_refined": False, } return { "subsector": label, "subsector_path": f"{normalized_group} > {label}", "subsector_source": source, "subsector_refined": True, }