260 lines
9.5 KiB
Python
260 lines
9.5 KiB
Python
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}并见,宜防一端偏盛"
|