from __future__ import annotations from datetime import date, datetime from typing import Any from lunar_python import Solar ELEMENTS = ("木", "火", "土", "金", "水") STEM_MOVEMENT = { "甲": "土", "己": "土", "乙": "金", "庚": "金", "丙": "水", "辛": "水", "丁": "木", "壬": "木", "戊": "火", "癸": "火", } STEM_ELEMENT = { "甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土", "己": "土", "庚": "金", "辛": "金", "壬": "水", "癸": "水", } BRANCH_ELEMENT = { "子": "水", "丑": "土", "寅": "木", "卯": "木", "辰": "土", "巳": "火", "午": "火", "未": "土", "申": "金", "酉": "金", "戌": "土", "亥": "水", } SITIAN = { "子": "少阴君火", "午": "少阴君火", "丑": "太阴湿土", "未": "太阴湿土", "寅": "少阳相火", "申": "少阳相火", "卯": "阳明燥金", "酉": "阳明燥金", "辰": "太阳寒水", "戌": "太阳寒水", "巳": "厥阴风木", "亥": "厥阴风木", } ZAIQUAN = { "少阴君火": "阳明燥金", "太阴湿土": "太阳寒水", "少阳相火": "厥阴风木", "阳明燥金": "少阴君火", "太阳寒水": "太阴湿土", "厥阴风木": "少阳相火", } QI_SEQUENCE = ("厥阴风木", "少阴君火", "太阴湿土", "少阳相火", "阳明燥金", "太阳寒水") HOST_SEQUENCE = ("厥阴风木", "少阴君火", "少阳相火", "太阴湿土", "阳明燥金", "太阳寒水") QI_ELEMENT = {name: name[-1] for name in QI_SEQUENCE} STEP_NAMES = ("初之气", "二之气", "三之气", "四之气", "五之气", "终之气") GENERATES = {"木": "火", "火": "土", "土": "金", "金": "水", "水": "木"} CONTROLS = {"木": "土", "土": "水", "水": "火", "火": "金", "金": "木"} CLIMATE = { "木": "风木疏动", "火": "热象渐显", "土": "湿滞偏重", "金": "燥气收敛", "水": "寒意潜行", } BEHAVIOR = { "木": ("求新与扩张感增强", "防止把萌芽误作主升", "先写清验证条件"), "火": ("兴奋与急迫感增强", "防止把一致误作确定", "延迟一次冲动决策"), "土": ("对确定性的需求增强", "防止把犹豫误作耐心", "按失效条件做减法"), "金": ("警觉与裁决感增强", "防止过早否定修复", "区分逻辑失效与波动"), "水": ("避险与不确定感增强", "防止放大最坏想象", "降低频率并保留预案"), } INDUSTRIES = { "木": ("农业", "林业", "医药", "教育", "纺织", "家居"), "火": ("电力", "新能源", "电子", "半导体", "通信", "传媒"), "土": ("地产", "建筑", "建材", "食品", "零售", "仓储"), "金": ("银行", "证券", "保险", "有色", "机械", "军工"), "水": ("航运", "物流", "水务", "饮料", "化工", "旅游"), } def build(trade_date: str, profile: Any | None = None) -> dict[str, Any]: parsed = date.fromisoformat(trade_date) solar = Solar.fromYmdHms(parsed.year, parsed.month, parsed.day, 12, 0, 0) lunar = solar.getLunar() year_gz = lunar.getYearInGanZhiExact() month_gz = lunar.getMonthInGanZhiExact() day_gz = lunar.getDayInGanZhiExact() sitian = SITIAN[year_gz[1]] zaiquan = ZAIQUAN[sitian] step = _qi_step(lunar, solar.toYmd()) host = HOST_SEQUENCE[step - 1] guest = QI_SEQUENCE[(QI_SEQUENCE.index(sitian) - 2 + step - 1) % 6] sitian_weight, zaiquan_weight = (15, 5) if step <= 3 else (5, 15) layers = ( _layer( "年纲", ( (STEM_MOVEMENT[year_gz[0]], 30), (QI_ELEMENT[sitian], sitian_weight), (QI_ELEMENT[zaiquan], zaiquan_weight), ), ), _layer("客主加临", ((QI_ELEMENT[host], 20), (QI_ELEMENT[guest], 25))), _layer("日辰触发", ((STEM_MOVEMENT[day_gz[0]], 2.5), (BRANCH_ELEMENT[day_gz[1]], 2.5))), ) totals = {element: sum(layer["weights"][element] for layer in layers) for element in ELEMENTS} balance = sorted( ( {"element": element, "score": score, "percent": round(score)} for element, score in totals.items() ), key=lambda item: item["score"], reverse=True, ) primary, secondary = balance[0]["element"], balance[1]["element"] phrase = f"{CLIMATE[primary]}·{CLIMATE[secondary]}" behavior = BEHAVIOR[primary] return { "date": trade_date, "lunar_date": f"农历{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}", "pillars": {"year": year_gz, "month": month_gz, "day": day_gz}, "solar_term": { "current": lunar.getPrevJieQi().getName(), "next": lunar.getNextJieQi().getName(), }, "phrase": phrase, "movement": { "element": STEM_MOVEMENT[year_gz[0]], "tendency": "太过" if year_gz[0] in "甲丙戊庚壬" else "不及", }, "six_qi": { "sitian": sitian, "zaiquan": zaiquan, "step": step, "step_name": STEP_NAMES[step - 1], "host": host, "guest": guest, }, "layers": [ { "label": layers[0]["label"], "dominant": layers[0]["dominant"], "summary": f"{STEM_MOVEMENT[year_gz[0]]}运为纲,司天{sitian},在泉{zaiquan}", }, { "label": layers[1]["label"], "dominant": layers[1]["dominant"], "summary": ( f"客{guest}加临主{host},{_relation(QI_ELEMENT[guest], QI_ELEMENT[host])}" ), }, { "label": layers[2]["label"], "dominant": layers[2]["dominant"], "summary": f"{day_gz}日,日干与日支只作轻量触发", }, ], "balance": balance, "human_field": { "emotional_tendency": behavior[0], "risk": behavior[1], "balancing_action": behavior[2], "generation_control": _generation_control(primary, secondary), }, "personal": _personal(profile, (primary, secondary)), "sector_catalog": [ {"element": element, "industries": list(INDUSTRIES[element])} for element in ELEMENTS ], "notice": "五行气场是传统历法与市场行为的象征性观察,不代表可验证的因果关系。", } def _personal(profile: Any | None, dominant: tuple[str, str]) -> dict[str, Any] | None: if profile is None: return None born = datetime.strptime(f"{profile.birth_date}T{profile.birth_time}", "%Y-%m-%dT%H:%M") lunar = Solar.fromYmdHms(born.year, born.month, born.day, born.hour, born.minute, 0).getLunar() eight = lunar.getEightChar() pillars = (eight.getYear(), eight.getMonth(), eight.getDay(), eight.getTime()) weights = {element: 0.0 for element in ELEMENTS} for index, pillar in enumerate(pillars): weights[STEM_ELEMENT[pillar[0]]] += 1 weights[BRANCH_ELEMENT[pillar[1]]] += 1.5 if index == 1 else 1 day_element = STEM_ELEMENT[eight.getDayGan()] supportive = { day_element, next(element for element, target in GENERATES.items() if target == day_element), } hits = [element for element in dominant if element in supportive] return { "day_master_element": day_element, "balance": sorted(weights.items(), key=lambda item: item[1], reverse=True), "tone": f"当日主气中{'、'.join(hits)}较合个人生扶倾向" if hits else "当日主气与个人生扶倾向交错,宜先察情绪再行动", "notice": "个人信息只用于本地派生计算,页面不回显出生日期、时辰和性别。", } def _layer(label: str, parts: tuple[tuple[str, float], ...]) -> dict[str, Any]: weights = {element: 0.0 for element in ELEMENTS} for element, amount in parts: weights[element] += amount return {"label": label, "weights": weights, "dominant": max(weights, key=weights.get)} def _qi_step(lunar: Any, ymd: str) -> int: current = int(ymd.replace("-", "")) boundaries = [ int(lunar.getJieQiTable()[name].toYmd().replace("-", "")) for name in ("大寒", "春分", "小满", "大暑", "秋分", "小雪") if lunar.getJieQiTable().get(name) is not None ] if len(boundaries) != 6 or current < boundaries[0] or current >= boundaries[5]: return 6 if len(boundaries) == 6 else 1 return next( (index + 1 for index in range(5) if boundaries[index] <= current < boundaries[index + 1]), 6 ) def _relation(guest: str, host: str) -> str: if guest == host: return "客主同气" if GENERATES[guest] == host: return "客生主,气机相接" if GENERATES[host] == guest: return "主生客,时令外泄" if CONTROLS[guest] == host: return "客克主,外来变化偏强" return "主克客,时令与来气相持" def _generation_control(primary: str, secondary: str) -> str: if GENERATES[primary] == secondary: return f"{primary}生{secondary},主气向次气流转" if CONTROLS[primary] == secondary: return f"{primary}克{secondary},主次之气相制" if GENERATES[secondary] == primary: return f"{secondary}生{primary},次气助主" if CONTROLS[secondary] == primary: return f"{secondary}克{primary},次气牵制主气" return f"{primary}{secondary}并见,宜防一端偏盛"