Files
xiaobaifupan/heaven_engine.py

1183 lines
50 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import math
import sys
from datetime import datetime
from functools import lru_cache
from pathlib import Path
from typing import Any
APP_DIR = Path(__file__).resolve().parent
VENDOR_DIR = APP_DIR / "vendor"
ICHING_DATA_FILE = APP_DIR / "data" / "iching_zh.json"
if str(VENDOR_DIR) not in sys.path:
sys.path.insert(0, str(VENDOR_DIR))
from lunar_python import Solar # noqa: E402
from lunar_python.util import LunarUtil # noqa: E402
TRIGRAM_NAMES = {
(1, 1, 1): "乾",
(1, 1, 0): "兑",
(1, 0, 1): "离",
(1, 0, 0): "震",
(0, 1, 1): "巽",
(0, 1, 0): "坎",
(0, 0, 1): "艮",
(0, 0, 0): "坤",
}
LINE_POSITIONS = ("初爻", "二爻", "三爻", "四爻", "五爻", "上爻")
LINE_ROLES = (
("地", "内", "个股内核"),
("地", "外", "个股外显"),
("人", "内", "行业内核"),
("人", "外", "行业外显"),
("天", "内", "指数内核"),
("天", "外", "指数外显"),
)
STEM_MOVEMENT = {
"甲": "土", "己": "土",
"乙": "金", "庚": "金",
"丙": "水", "辛": "水",
"丁": "木", "壬": "木",
"戊": "火", "癸": "火",
}
MOVEMENT_PAIR = {
"土": "甲己化土",
"金": "乙庚化金",
"水": "丙辛化水",
"木": "丁壬化木",
"火": "戊癸化火",
}
YANG_STEMS = set("甲丙戊庚壬")
STEM_ELEMENT = {
"甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土",
"己": "土", "庚": "金", "辛": "金", "壬": "水", "癸": "水",
}
BRANCH_ELEMENT = {
"子": "水", "丑": "土", "寅": "木", "卯": "木", "辰": "土", "巳": "火",
"午": "火", "未": "土", "申": "金", "酉": "金", "戌": "土", "亥": "水",
}
SUIHUI_BRANCHES = set("子丑卯辰午未酉戌")
SITIAN = {
"子": "少阴君火", "午": "少阴君火",
"丑": "太阴湿土", "未": "太阴湿土",
"寅": "少阳相火", "申": "少阳相火",
"卯": "阳明燥金", "酉": "阳明燥金",
"辰": "太阳寒水", "戌": "太阳寒水",
"巳": "厥阴风木", "亥": "厥阴风木",
}
ZAIQUAN = {
"少阴君火": "阳明燥金",
"太阴湿土": "太阳寒水",
"少阳相火": "厥阴风木",
"阳明燥金": "少阴君火",
"太阳寒水": "太阴湿土",
"厥阴风木": "少阳相火",
}
# 客气次序(一阴→二阴→三阴→一阳→二阳→三阳)。
QI_SEQUENCE = ("厥阴风木", "少阴君火", "太阴湿土", "少阳相火", "阳明燥金", "太阳寒水")
# 主气次序(固定,按五行相生:木→君火→相火→湿土→燥金→寒水)。
HOST_QI_SEQUENCE = ("厥阴风木", "少阴君火", "少阳相火", "太阴湿土", "阳明燥金", "太阳寒水")
QI_ELEMENT = {
"厥阴风木": "木", "少阴君火": "火", "太阴湿土": "土",
"少阳相火": "火", "阳明燥金": "金", "太阳寒水": "水",
}
STEP_NAMES = ("初之气", "二之气", "三之气", "四之气", "五之气", "终之气")
PHASE_INFO = {
"木": {"motion": "生发、扩散、延展", "mind": "更愿意尝试新方向,也容易高估成长斜率"},
"火": {"motion": "显化、加速、躁动", "mind": "注意力集中、追逐速度,也容易冲动和过度一致"},
"土": {"motion": "承载、黏合、迟滞", "mind": "偏好确定和稳定,也可能出现犹豫与路径依赖"},
"金": {"motion": "收敛、裁决、肃降", "mind": "纪律和风险意识增强,也容易形成快速杀估值"},
"水": {"motion": "流动、潜藏、下行", "mind": "资金更重视流动性和退路,也可能放大恐惧传染"},
}
PHASE_BEHAVIOR = {
"木": {
"emotion": "求新与扩张感增强,容易对新题材迅速产生期待",
"bias": "倾向先看到成长空间,再补风险验证",
"operation": "更想试仓、开新方向或给趋势更高估值",
"risk": "防止把萌芽当成主升,把想象力当成确认",
"balance": "先写清验证条件,等分歧后的承接再决定是否加码",
},
"火": {
"emotion": "兴奋、急迫和表现欲更容易被放大,群体注意力趋于集中",
"bias": "倾向追逐速度与一致性,低估高位拥挤和冲动成本",
"operation": "更容易追涨、抢先手、放宽原有仓位上限",
"risk": "防止情绪高潮时把一致误作确定,把速度误作安全",
"balance": "延迟一次下单冲动,用成交承接和次日反馈替代情绪确认",
},
"土": {
"emotion": "对确定性和安全感的需求上升,也容易迟疑、黏滞",
"bias": "倾向依赖熟悉路径,对已经持有的判断更难松手",
"operation": "更容易守仓、等确认,或因不愿认错而延迟处理",
"risk": "防止把稳定感当作低风险,把犹豫当作耐心",
"balance": "把持仓理由量化,触发失效条件时按计划减法处理",
},
"金": {
"emotion": "警觉、挑剔和裁决感增强,容错意愿下降",
"bias": "倾向快速分辨强弱,也可能过早否定尚在修复的机会",
"operation": "更容易止损、兑现、收缩仓位并集中到辨识度高的标的",
"risk": "防止在恐慌扩散时机械割裂,也防止过度追求完美买点",
"balance": "区分逻辑失效与价格波动,给修复保留一个观察窗口",
},
"水": {
"emotion": "不确定感与避险意识上升,消息和恐惧更容易传染",
"bias": "倾向先寻找退路,可能放大流动性风险或反复试探",
"operation": "更容易降仓、观望、快进快出,偏好有流动性的方向",
"risk": "防止因想象最坏结果而在低流动性时点失去判断",
"balance": "降低频率,保留现金与预案,只处理能清楚定义风险的交易",
},
}
ELEMENT_GENERATES = {"木": "火", "火": "土", "土": "金", "金": "水", "水": "木"}
ELEMENT_CONTROLS = {"木": "土", "土": "水", "水": "火", "火": "金", "金": "木"}
SECTOR_PHASE_RULES = {
"木": (
# 植物生长类 + 仁术(医) + 教化(教育) + 纤维文书
"农业", "种植", "种业", "林业", "园林", "畜牧", "养殖", "饲料",
"医药", "中药", "生物医药", "创新药", "医疗", "疫苗",
"教育", "培训", "出版", "图书",
"纺织", "服装", "服饰", "家纺", "造纸", "印刷", "包装",
"家具", "家居", "木材", "烟草",
),
"火": (
# 光热能源 + 电子传媒 + 炉灶
"电力", "火电", "光伏", "太阳能", "风电", "储能", "电池", "锂电",
"充电桩", "新能源", "核电", "煤炭", "石油", "石化", "燃气",
"电子", "半导体", "芯片", "集成电路", "消费电子", "光学", "光电",
"显示", "面板", "通信", "计算机", "软件", "互联网", "游戏",
"人工智能", "数据", "云计算", "传媒", "影视", "广告", "娱乐", "直播",
),
"土": (
# 不动产 + 营造 + 稼穑饮食(土主养育)
"地产", "房地产", "物业", "建筑", "基建", "工程", "路桥",
"建材", "水泥", "玻璃", "陶瓷", "混凝土", "管材", "防水",
"食品", "乳业", "肉制品", "调味品", "农产品加工",
"零售", "百货", "仓储",
),
"金": (
# 金属机械 + 财帛裁决 + 兵戈肃杀
"银行", "证券", "保险", "期货", "信托", "金融", "支付",
"钢铁", "有色", "金属", "贵金属", "黄金", "稀土",
"机械", "设备", "机床", "机器人", "仪器", "仪表",
"汽车", "整车", "零部件", "家电", "五金",
"军工", "国防", "兵器", "船舶", "航天",
),
"水": (
# 流动运输 + 液体 + 商旅(水主流、主智)
"航运", "港口", "物流", "快递", "运输", "航空", "机场",
"水务", "供水", "污水", "水利", "环保",
"饮料", "白酒", "啤酒", "黄酒",
"化工", "化学", "化纤",
"旅游", "酒店", "餐饮", "水产", "渔业", "贸易", "商贸",
),
}
def build_market_hexagram(
dashboard: dict[str, Any],
recent_history: list[dict[str, Any]],
index_context: dict[str, Any] | None = None,
sector_name: str = "",
stock_code: str = "",
external_stock: dict[str, Any] | None = None,
external_sector: dict[str, Any] | None = None,
) -> dict[str, Any]:
sectors = list(dashboard.get("sectors") or [])
limits = list(dashboard.get("limits") or [])
broken = list(dashboard.get("broken") or [])
down_limits = list(dashboard.get("down_limits") or [])
normalized_sector = sector_name.strip().lower()
external_sector = external_sector or {}
selected_sector = next(
(
item for item in sectors
if str(item.get("name") or "").strip().lower() == normalized_sector
or (normalized_sector and normalized_sector in str(item.get("name") or "").strip().lower())
),
None,
)
if external_sector:
selected_sector = external_sector
external_stock = external_stock or {}
external_stock_sector = str(external_stock.get("sector") or "").strip()
if selected_sector is None and external_stock_sector:
selected_sector = next((item for item in sectors if item.get("name") == external_stock_sector), None)
if selected_sector is None and external_stock:
selected_sector = {
"name": external_stock_sector or sector_name.strip() or "个股所属行业",
"leader": external_stock.get("name") or "--",
"change": external_stock.get("change") or 0,
"strength": max(0, min(100, 50 + float(external_stock.get("change") or 0) * 3)),
"amount_billion": external_stock.get("amount_billion") or 0,
"count": 0,
"max_streak": 0,
}
selected_sector = selected_sector or (sectors[0] if sectors else {})
actual_sector = str(selected_sector.get("name") or "暂无热点")
sector_stocks = [row for row in limits + broken + down_limits if row.get("sector") == actual_sector]
selected_stock = next((row for row in sector_stocks if str(row.get("code")) == stock_code), None)
if selected_stock is None and external_stock:
selected_stock = external_stock
if selected_stock is None and selected_sector.get("leader"):
selected_stock = next(
(row for row in sector_stocks if row.get("name") == selected_sector.get("leader")),
None,
)
selected_stock = selected_stock or (sector_stocks[0] if sector_stocks else (limits[0] if limits else {}))
scores = _market_line_scores(
dashboard,
recent_history,
index_context or {},
selected_sector,
selected_stock,
limits,
)
values = [_score_to_line(item["score"]) for item in scores]
hexagram = hexagram_from_lines(values)
for index, (line, score) in enumerate(zip(hexagram["lines"], scores)):
talent, layer, role = LINE_ROLES[index]
line.update(
{
"talent": talent,
"layer": layer,
"role": role,
"score": round(score["score"], 3),
"evidence": score["evidence"],
}
)
pair_readings = []
for label, inner_index, outer_index in (("地·个股", 0, 1), ("人·行业", 2, 3), ("天·指数", 4, 5)):
inner = scores[inner_index]["score"]
outer = scores[outer_index]["score"]
if inner >= 0 and outer >= 0:
state = "内外相应,势有承载"
elif inner < 0 <= outer:
state = "外强内弱,表里有差"
elif inner >= 0 > outer:
state = "内强外抑,势待显化"
else:
state = "内外皆弱,宜守不宜躁"
pair_readings.append({"level": label, "state": state, "inner": round(inner, 3), "outer": round(outer, 3)})
options = []
for sector in sectors[:20]:
name = str(sector.get("name") or "")
stocks = [row for row in limits + broken + down_limits if row.get("sector") == name]
options.append(
{
"name": name,
"leader": sector.get("leader") or "",
"stocks": [
{"code": str(row.get("code") or ""), "name": row.get("name") or "--", "status": row.get("status") or ""}
for row in stocks[:20]
],
}
)
average_score = sum(item["score"] for item in scores) / 6
moving_names = [LINE_POSITIONS[index - 1] for index in hexagram["moving_lines"]]
movement = {
"moving_lines": hexagram["moving_lines"],
"moving_names": moving_names,
"label": (
f"{'、'.join(moving_names)}动,{hexagram['name']}{hexagram['transformed']['name']}"
if moving_names
else f"无动爻,守{hexagram['name']}本势"
),
"explanation": "本卦看当下之势,动爻看势的转折处,之卦看变化所趋。",
}
return {
"data_trade_date": str(dashboard.get("meta", {}).get("trade_date") or ""),
"sector": actual_sector,
"sector_code": str(selected_sector.get("code") or ""),
"sector_taxonomy": str(selected_sector.get("taxonomy") or ""),
"stock": {
"code": str(selected_stock.get("code") or ""),
"name": selected_stock.get("name") or "--",
"status": selected_stock.get("status") or "",
},
"selection_notice": "",
"hexagram": hexagram,
"movement": movement,
"pair_readings": pair_readings,
"momentum_score": round(average_score * 100),
"momentum_label": _momentum_label(average_score),
"sector_options": options,
"index_context": index_context or {},
}
def build_manual_market_hexagram(
values: list[int],
data_trade_date: str,
sector: dict[str, Any] | None,
stock: dict[str, Any] | None,
index_context: dict[str, Any] | None = None,
note: str = "",
) -> dict[str, Any]:
"""Build an explicitly user-calibrated chart without pretending it is market data."""
hexagram = hexagram_from_lines(values)
score_map = {6: -0.85, 8: -0.35, 7: 0.35, 9: 0.85}
scores = [score_map[value] for value in values]
value_names = {6: "老阴·动", 8: "少阴·静", 7: "少阳·静", 9: "老阳·动"}
for index, line in enumerate(hexagram["lines"]):
talent, layer, role = LINE_ROLES[index]
line.update(
{
"talent": talent,
"layer": layer,
"role": role,
"score": scores[index],
"evidence": [f"用户手动校准为{value_names[values[index]]}"],
}
)
pair_readings = []
for label, inner_index, outer_index in (("地·个股", 0, 1), ("人·行业", 2, 3), ("天·指数", 4, 5)):
inner, outer = scores[inner_index], scores[outer_index]
if inner >= 0 and outer >= 0:
state = "内外相应,势有承载"
elif inner < 0 <= outer:
state = "外强内弱,表里有差"
elif inner >= 0 > outer:
state = "内强外抑,势待显化"
else:
state = "内外皆弱,宜守不宜躁"
pair_readings.append({"level": label, "state": state, "inner": inner, "outer": outer})
moving_names = [LINE_POSITIONS[index - 1] for index in hexagram["moving_lines"]]
movement = {
"moving_lines": hexagram["moving_lines"],
"moving_names": moving_names,
"label": (
f"{'、'.join(moving_names)}动,{hexagram['name']}{hexagram['transformed']['name']}"
if moving_names else f"无动爻,守{hexagram['name']}本势"
),
"explanation": "本卦看当下之势,动爻看势的转折处,之卦看变化所趋。",
}
average_score = sum(scores) / 6
sector = sector or {}
stock = stock or {}
return {
"data_trade_date": data_trade_date,
"sector": str(sector.get("name") or stock.get("sector") or "--"),
"sector_code": str(sector.get("code") or ""),
"sector_taxonomy": str(sector.get("taxonomy") or ""),
"stock": {
"code": str(stock.get("code") or ""),
"name": str(stock.get("name") or "--"),
"status": str(stock.get("status") or ""),
},
"selection_notice": "",
"hexagram": hexagram,
"movement": movement,
"pair_readings": pair_readings,
"momentum_score": round(average_score * 100),
"momentum_label": _momentum_label(average_score),
"sector_options": [],
"index_context": index_context or {},
"manual_calibration": True,
"calibration_note": note,
}
def build_five_phase_field(
trade_date: str,
sector_phase_overrides: dict[str, str] | None = None,
) -> dict[str, Any]:
"""构建指定日期的五运六气场。
本系统约定:六气阶段以大寒为岁首步进,司天在泉随年干支以立春为界切换;
大寒至立春之间,六气已入新一年初之气,司天在泉仍属旧年。
"""
compact = trade_date.replace("-", "")
if len(compact) != 8 or not compact.isdigit():
raise ValueError("日期格式应为 YYYY-MM-DD。")
year, month, day = int(compact[:4]), int(compact[4:6]), int(compact[6:])
# 公共气场以日期为最小粒度。固定取正午只为构造历法对象,不引入时辰权重。
solar = Solar.fromYmdHms(year, month, day, 12, 0, 0)
lunar = solar.getLunar()
year_gz = lunar.getYearInGanZhiExact()
month_gz = lunar.getMonthInGanZhiExact()
day_gz = lunar.getDayInGanZhiExact()
year_stem, year_branch = year_gz[0], year_gz[1]
movement_phase = STEM_MOVEMENT[year_stem]
movement_tendency = "太过" if year_stem in YANG_STEMS else "不及"
sitian = SITIAN[year_branch]
zaiquan = ZAIQUAN[sitian]
step = _current_qi_step(lunar, solar.toYmd())
host_qi = HOST_QI_SEQUENCE[step - 1]
sitian_index = QI_SEQUENCE.index(sitian)
guest_qi = QI_SEQUENCE[(sitian_index - 2 + step - 1) % 6]
prev_jie_qi = lunar.getPrevJieQi()
next_jie_qi = lunar.getNextJieQi()
# 年纲由中运与岁气共同建立。岁半以前司天为主,岁半以后在泉为主;
# 另一端仍保留背景作用,避免把天地升降误解为截然切断。
sitian_weight, zaiquan_weight = (15, 5) if step <= 3 else (5, 15)
year_weights = {element: 0.0 for element in PHASE_INFO}
_add_phase(year_weights, movement_phase, 30)
_add_phase(year_weights, QI_ELEMENT[sitian], sitian_weight)
_add_phase(year_weights, QI_ELEMENT[zaiquan], zaiquan_weight)
current_qi_weights = {element: 0.0 for element in PHASE_INFO}
_add_phase(current_qi_weights, QI_ELEMENT[host_qi], 20)
_add_phase(current_qi_weights, QI_ELEMENT[guest_qi], 25)
day_weights = {element: 0.0 for element in PHASE_INFO}
_add_phase(day_weights, STEM_MOVEMENT[day_gz[0]], 2.5)
_add_phase(day_weights, BRANCH_ELEMENT[day_gz[1]], 2.5)
weights = {
element: year_weights[element] + current_qi_weights[element] + day_weights[element]
for element in PHASE_INFO
}
total = sum(weights.values()) or 1
balance = [
{
"element": element,
"score": score,
"percent": round(score / total * 100),
**PHASE_INFO[element],
}
for element, score in sorted(weights.items(), key=lambda item: item[1], reverse=True)
]
overrides = sector_phase_overrides or {}
sector_catalog = _sector_phase_catalog(overrides)
dominant = balance[0]
secondary = balance[1]
year_dominant = _dominant_phase(year_weights)
current_qi_dominant = _dominant_phase(current_qi_weights)
day_dominant = _dominant_phase(day_weights)
guest_host_relation = _guest_host_relation(host_qi, guest_qi)
annual_pattern = _annual_qi_pattern(
movement_phase,
QI_ELEMENT[sitian],
year_branch,
)
annual_pattern_suffix = f"{annual_pattern['primary']}" if annual_pattern["primary"] else ""
ruling_qi = sitian if step <= 3 else zaiquan
ruling_label = "司天" if step <= 3 else "在泉"
alignment = ""
if guest_qi == sitian:
alignment = "司天同位"
elif guest_qi == zaiquan:
alignment = "在泉同位"
dominant_behavior = PHASE_BEHAVIOR[dominant["element"]]
secondary_behavior = PHASE_BEHAVIOR[secondary["element"]]
calendar_date = f"{year:04d}-{month:02d}-{day:02d}"
human_field = {
"summary": (
f"年以{year_dominant}为纲,当前{STEP_NAMES[step - 1]}{ruling_label}{ruling_qi}主其半岁,"
f"客主呈{guest_host_relation['label']},日由{day_dominant}触发;"
f"合看以{dominant['element']}气偏显、{secondary['element']}气相随。{dominant_behavior['emotion']}。"
),
"emotional_tendency": [dominant_behavior["emotion"], secondary_behavior["emotion"]],
"decision_biases": [dominant_behavior["bias"], secondary_behavior["bias"]],
"operation_tendency": dominant_behavior["operation"],
"risk_reminders": [dominant_behavior["risk"], secondary_behavior["risk"]],
"balancing_actions": [dominant_behavior["balance"], secondary_behavior["balance"]],
}
return {
"date": calendar_date,
"lunar_date": f"农历{lunar.getMonthInChinese()}{lunar.getDayInChinese()}",
"pillars": {"year": year_gz, "month": month_gz, "day": day_gz},
"movement": {
"phase": movement_phase,
"tendency": movement_tendency,
"label": f"{movement_phase}{movement_tendency}",
"basis": f"{year_stem}{movement_phase}运,{year_stem}{'阳干' if year_stem in YANG_STEMS else '阴干'}",
},
"six_qi": {
"sitian": sitian,
"zaiquan": zaiquan,
"step": step,
"step_name": STEP_NAMES[step - 1],
"host_qi": host_qi,
"guest_qi": guest_qi,
"ruling": ruling_label,
"ruling_qi": ruling_qi,
"alignment": alignment,
},
"solar_terms": {
"current": prev_jie_qi.getName(),
"current_at": prev_jie_qi.getSolar().toYmdHms(),
"next": next_jie_qi.getName(),
"next_at": next_jie_qi.getSolar().toYmdHms(),
},
"framework": {
"principle": "先立年纲,再察客气加临主气;岁半以前司天为主,岁半以后在泉为主,日辰只作触发。六气自大寒步进,岁气以立春为界。",
"weights": {
"year_movement": 30,
"sitian_zaiquan": 20,
"sitian": sitian_weight,
"zaiquan": zaiquan_weight,
"host_qi": 20,
"guest_qi": 25,
"day": 5,
},
"relations": {
"guest_host": guest_host_relation,
"annual_pattern": annual_pattern,
"alignment": alignment,
"ruling": {
"label": ruling_label,
"qi": ruling_qi,
"summary": f"当前由{ruling_label}{ruling_qi}主其半岁,另一端退居背景。",
},
},
"layers": [
{
"id": "year",
"label": "年纲",
"weight": 50,
"dominant": year_dominant,
"summary": (
f"{MOVEMENT_PAIR[movement_phase]}{movement_phase}{movement_tendency}"
f"{ruling_label}{ruling_qi}当权"
f"{annual_pattern_suffix}"
),
"balance": _phase_distribution(year_weights),
},
{
"id": "current",
"label": "客主加临",
"weight": 45,
"dominant": current_qi_dominant,
"summary": (
f"当前{STEP_NAMES[step - 1]},客{guest_qi}加临主{host_qi}"
f"{guest_host_relation['label']}{guest_host_relation['tendency']}"
),
"balance": _phase_distribution(current_qi_weights),
},
{
"id": "day",
"label": "日辰触发",
"weight": 5,
"dominant": day_dominant,
"summary": f"{day_gz}日,{_movement_label(day_gz[0])}{day_gz[1]}{BRANCH_ELEMENT[day_gz[1]]}、应{SITIAN[day_gz[1]]}",
"balance": _phase_distribution(day_weights),
},
],
},
"balance": balance,
"human_field": human_field,
"sector_catalog": sector_catalog,
"notice": "五行气场是传统历法与市场行为的象征性观察,不代表可验证的因果关系。",
}
def build_personal_field(
birth_datetime: str,
gender: str,
current_date: str,
current_field: dict[str, Any] | None = None,
) -> dict[str, Any]:
try:
born = datetime.strptime(birth_datetime, "%Y-%m-%dT%H:%M")
except ValueError as exc:
raise ValueError("出生时间格式应为 YYYY-MM-DDTHH:MM。") from exc
if not 1900 <= born.year <= 2100:
raise ValueError("出生年份应在 1900 至 2100 年之间。")
if gender not in {"male", "female", "unspecified"}:
raise ValueError("性别选项不正确。")
solar = Solar.fromYmdHms(born.year, born.month, born.day, born.hour, born.minute, 0)
lunar = solar.getLunar()
eight = lunar.getEightChar()
pillars = {
"year": eight.getYear(),
"month": eight.getMonth(),
"day": eight.getDay(),
"time": eight.getTime(),
}
visible_elements = {element: 0.0 for element in PHASE_INFO}
for key, pillar in pillars.items():
visible_elements[STEM_ELEMENT[pillar[0]]] += 1
visible_elements[BRANCH_ELEMENT[pillar[1]]] += 1.5 if key == "month" else 1
total = sum(visible_elements.values()) or 1
element_balance = [
{"element": element, "score": round(score, 1), "percent": round(score / total * 100)}
for element, score in sorted(visible_elements.items(), key=lambda item: item[1], reverse=True)
]
day_master = eight.getDayGan()
day_element = STEM_ELEMENT[day_master]
resource_element = next(element for element, generated in ELEMENT_GENERATES.items() if generated == day_element)
output_element = ELEMENT_GENERATES[day_element]
wealth_element = ELEMENT_CONTROLS[day_element]
officer_element = next(element for element, controlled in ELEMENT_CONTROLS.items() if controlled == day_element)
support_score = visible_elements[day_element] + visible_elements[resource_element]
if support_score < total * 0.42:
strength = "偏弱"
favorable = [resource_element, day_element]
caution = [officer_element, wealth_element, output_element]
balance_note = "日主支持偏少,简化算法倾向先取生扶,再看泄耗与制约是否过强。"
elif support_score > total * 0.62:
strength = "偏强"
favorable = [output_element, wealth_element, officer_element]
caution = [day_element, resource_element]
balance_note = "日主支持偏多,简化算法倾向用泄、耗、制来恢复流动。"
else:
strength = "相对平衡"
favorable = [output_element, wealth_element]
caution = [element_balance[0]["element"]]
balance_note = "五行支持与消耗接近,简化算法更看重当下偏盛元素的调节。"
ten_gods = {
"year": {"stem": eight.getYearShiShenGan(), "branches": eight.getYearShiShenZhi()},
"month": {"stem": eight.getMonthShiShenGan(), "branches": eight.getMonthShiShenZhi()},
"day": {"stem": "日主", "branches": eight.getDayShiShenZhi()},
"time": {"stem": eight.getTimeShiShenGan(), "branches": eight.getTimeShiShenZhi()},
}
ten_god_roles = {
day_element: "比劫",
resource_element: "印星",
output_element: "食伤",
wealth_element: "财星",
officer_element: "官杀",
}
compact = current_date.replace("-", "")
if len(compact) != 8 or not compact.isdigit():
raise ValueError("当前日期格式应为 YYYY-MM-DD。")
current_solar = Solar.fromYmdHms(int(compact[:4]), int(compact[4:6]), int(compact[6:]), 12, 0, 0)
current_lunar = current_solar.getLunar()
current_pillars = {
"year": current_lunar.getYearInGanZhiExact(),
"month": current_lunar.getMonthInGanZhiExact(),
"day": current_lunar.getDayInGanZhiExact(),
}
current_ten_gods = {
key: {
"pillar": pillar,
"stem": LunarUtil.SHI_SHEN.get(day_master + pillar[0]) or "--",
"branches": [LunarUtil.SHI_SHEN.get(day_master + gan) or "--" for gan in LunarUtil.ZHI_HIDE_GAN.get(pillar[1], [])],
}
for key, pillar in current_pillars.items()
}
field = current_field or build_five_phase_field(current_date)
dominant_elements = [item["element"] for item in field.get("balance", [])[:2]]
favorable_hits = [element for element in dominant_elements if element in favorable]
caution_hits = [element for element in dominant_elements if element in caution]
if favorable_hits and not caution_hits:
personal_tone = f"当日偏显的{'、'.join(dominant_elements)}中,{'、'.join(favorable_hits)}较合你的平衡倾向,主观上更容易感到有支点。"
operation_note = "顺手感可能增强,但仍应把它当作自我状态提醒,不宜因此放宽交易纪律。"
elif caution_hits and not favorable_hits:
personal_tone = f"当日偏显的{'、'.join(dominant_elements)}中,{'、'.join(caution_hits)}可能放大你的耗泄或压力感。"
operation_note = "更适合降低决策频率,尤其留意急于证明、犹豫不决或过早止损等惯性反应。"
else:
personal_tone = f"当日{'、'.join(dominant_elements)}并见,对你既有助力也有牵制,感受可能随情境切换。"
operation_note = "先辨认自己此刻是兴奋、恐惧还是执着,再决定是否需要行动。"
return {
"birth": {"datetime": birth_datetime, "gender": gender, "lunar": lunar.toString()},
"pillars": pillars,
"day_master": {"stem": day_master, "element": day_element, "strength": strength},
"ten_gods": ten_gods,
"ten_god_tendency": {
"favorable": [ten_god_roles[element] for element in favorable],
"caution": [ten_god_roles[element] for element in caution],
},
"element_balance": element_balance,
"balance_tendency": {
"favorable": favorable,
"caution": caution,
"note": balance_note,
"method": "按可见四柱五行、月令加权及日主生扶比例生成的简化平衡倾向,不等同于专业命理中的唯一喜用神结论。",
},
"current": {
"date": current_date,
"pillars": current_pillars,
"ten_gods": current_ten_gods,
"tone": personal_tone,
"operation_note": operation_note,
},
"notice": "个人结果仅供传统文化与自我观察使用。出生信息只在本机服务中计算。",
}
def hexagram_from_lines(values: list[int]) -> dict[str, Any]:
if len(values) != 6 or any(value not in {6, 7, 8, 9} for value in values):
raise ValueError("六爻必须由六、七、八、九组成,且从初爻到上爻排列。")
bits = tuple(1 if value % 2 else 0 for value in values)
transformed_values = [7 if value == 6 else 8 if value == 9 else value for value in values]
transformed_bits = tuple(1 if value % 2 else 0 for value in transformed_values)
data = _iching_data()
primary = data.get(str(bits))
transformed = data.get(str(transformed_bits))
if not primary or not transformed:
raise ValueError("卦象数据不完整。")
lines = []
line_items = list(primary["lines"].values())
for index, (value, item) in enumerate(zip(values, line_items)):
lines.append(
{
"position": index + 1,
"position_name": LINE_POSITIONS[index],
"value": value,
"yin_yang": "阳" if value % 2 else "阴",
"moving": value in {6, 9},
"line_name": item["name"],
"text": item["text"],
"image": item.get("image") or "",
}
)
inner = TRIGRAM_NAMES[bits[:3]]
outer = TRIGRAM_NAMES[bits[3:]]
transformed_inner = TRIGRAM_NAMES[transformed_bits[:3]]
transformed_outer = TRIGRAM_NAMES[transformed_bits[3:]]
return {
"name": primary["name"],
"text": primary["text"],
"image": primary.get("image") or "",
"inner_trigram": inner,
"outer_trigram": outer,
"lines": lines,
"moving_lines": [index + 1 for index, value in enumerate(values) if value in {6, 9}],
"transformed": {
"name": transformed["name"],
"text": transformed["text"],
"image": transformed.get("image") or "",
"inner_trigram": transformed_inner,
"outer_trigram": transformed_outer,
},
}
def _market_line_scores(
dashboard: dict[str, Any],
recent_history: list[dict[str, Any]],
index_context: dict[str, Any],
sector: dict[str, Any],
stock: dict[str, Any],
limits: list[dict[str, Any]],
) -> list[dict[str, Any]]:
overview = dashboard.get("overview") or {}
stock_amount = float(stock.get("amount_billion") or 0)
stock_intraday = bool(stock.get("realtime")) or stock.get("_quantitative_mode") == "intraday"
if stock_intraday and stock.get("activity_source"):
amount_rank = _clamp(float(stock.get("amount_percentile") or 0) / 100)
turnover_relative = _clamp(
(float(stock.get("turnover_relative") or 0) - 1) / 1.5,
-1,
1,
)
volume_activity = _clamp(
(float(stock.get("volume_activity_ratio") or 0) - 1) / 1.5,
-1,
1,
)
stock_inner = _clamp(
(amount_rank * 2 - 1) * 0.35
+ turnover_relative * 0.35
+ volume_activity * 0.30,
-1,
1,
)
else:
amounts = [float(item.get("amount_billion") or 0) for item in limits]
amount_rank = (
_clamp(float(stock.get("amount_percentile") or 0) / 100)
if "amount_percentile" in stock
else _percentile(stock_amount, amounts)
)
turnover = _clamp(float(stock.get("turnover_rate") or 0) / 20)
seal = _clamp(float(stock.get("seal_amount_million") or 0) / 15000)
stability = 1 - _clamp(float(stock.get("open_times") or 0) / 6)
stock_inner_raw = 0.32 * amount_rank + 0.22 * turnover + 0.25 * seal + 0.21 * stability
stock_inner = stock_inner_raw * 2 - 1
stock_change = _clamp(float(stock.get("change") or 0) / 10, -1, 1)
streak = _clamp(float(stock.get("streak") or 0) / 5)
status_adjustment = -0.7 if stock.get("status") == "跌停" else -0.25 if stock.get("status") == "炸板" else 0.15
stock_outer = _clamp(stock_change * 0.7 + streak * 0.2 + status_adjustment, -1, 1)
rotation = next(
(item for item in dashboard.get("sector_rotation") or [] if item.get("name") == sector.get("name")),
{},
)
sector_quantitative_mode = str(sector.get("_quantitative_mode") or "")
actual_sector_source = str(sector.get("source") or "").startswith("tushare_")
if (sector.get("realtime") and actual_sector_source) or sector_quantitative_mode == "intraday":
sector_change = float(sector.get("change") or 0)
sector_change_score = _clamp(sector_change / 5, -1, 1)
sector_up = float(sector.get("up_count") or 0)
sector_down = float(sector.get("down_count") or 0)
sector_breadth = _clamp(
(sector_up - sector_down) / max(sector_up + sector_down, 1), -1, 1
)
relative_turnover_score = _clamp(
(float(sector.get("relative_turnover") or 0) - 1) / 1.5,
-1,
1,
)
leading_score = _clamp(float(sector.get("leading_pct") or 0) / 10, -1, 1)
sector_inner = _clamp(
sector_breadth * 0.60 + relative_turnover_score * 0.40,
-1,
1,
)
sector_outer = _clamp(
sector_change_score * 0.90 + leading_score * 0.10,
-1,
1,
)
sector_inner_evidence = [
f"成分上涨 {int(sector_up)} 家、下跌 {int(sector_down)} 家",
f"平均换手 {float(sector.get('turnover_rate') or 0):.2f}%,相对市场 {float(sector.get('relative_turnover') or 0):.2f} 倍",
]
sector_outer_evidence = [
f"申万二级行业官方涨跌 {sector_change:+.2f}%",
f"领涨 {sector.get('leader') or '--'} {float(sector.get('leading_pct') or 0):+.2f}%",
]
elif actual_sector_source or sector_quantitative_mode == "historical":
sector_change = float(sector.get("change") or 0)
sector_change_score = _clamp(sector_change / 5, -1, 1)
member_equal_change = float(sector.get("member_equal_change") if sector.get("member_equal_change") is not None else sector_change)
member_change_score = _clamp(member_equal_change / 5, -1, 1)
sector_up = float(sector.get("up_count") or 0)
sector_down = float(sector.get("down_count") or 0)
if sector_up + sector_down:
sector_breadth = _clamp((sector_up - sector_down) / (sector_up + sector_down), -1, 1)
else:
sector_breadth = sector_change_score
leading_score = _clamp(float(sector.get("leading_pct") or 0) / 10, -1, 1)
sector_inner = _clamp(sector_breadth * 0.6 + member_change_score * 0.35 + leading_score * 0.05, -1, 1)
sector_outer = _clamp(sector_change_score * 0.9 + leading_score * 0.1, -1, 1)
sector_inner_evidence = [
f"行业上涨 {int(sector_up)} 家、下跌 {int(sector_down)} 家",
f"行业成分等权涨跌 {member_equal_change:+.2f}%",
]
sector_outer_evidence = [
f"{sector.get('name') or '--'}行业涨跌 {sector_change:+.2f}%",
f"领涨 {sector.get('leader') or '--'} {float(sector.get('leading_pct') or 0):+.2f}%",
]
else:
max_count = max([float(item.get("count") or 0) for item in dashboard.get("sectors") or []] or [1])
sector_count = _clamp(float(sector.get("count") or 0) / max_count)
sector_strength = _clamp(float(sector.get("strength") or 0) / 100)
sector_amount = _clamp(float(sector.get("amount_billion") or 0) / 100)
delta = _clamp(float(rotation.get("delta") or 0) / 8, -1, 1)
sector_inner = _clamp((sector_count * 0.35 + sector_strength * 0.35 + sector_amount * 0.2 + (delta + 1) / 2 * 0.1) * 2 - 1)
leader_change = _clamp(float(sector.get("change") or 0) / 10, -1, 1)
max_streak = _clamp(float(sector.get("max_streak") or 0) / 5)
sector_outer = _clamp(
leader_change * 0.45 + sector_strength * 0.25 + max_streak * 0.2 + delta * 0.1,
-1,
1,
)
sector_inner_evidence = [
f"{sector.get('name') or '--'}涨停 {int(sector.get('count') or 0)} 家,强度 {float(sector.get('strength') or 0):.0f}",
f"板块成交 {float(sector.get('amount_billion') or 0):.1f} 亿,家数变化 {float(rotation.get('delta') or 0):+.0f}",
]
sector_outer_evidence = [
f"领涨股 {sector.get('leader') or '--'},涨跌 {float(sector.get('change') or 0):+.2f}%",
f"最高 {int(sector.get('max_streak') or 0)} 板,轮动 {rotation.get('trend') or '暂无'}",
]
sentiment = _clamp(float(overview.get("sentiment_score") or 0) / 100)
seal_rate = _clamp(float(overview.get("seal_rate") or 0) / 100)
up_count = float(overview.get("up_count") or 0)
down_count = float(overview.get("down_count") or 0)
breadth = up_count / max(up_count + down_count, 1)
breadth_score = _clamp((breadth - 0.5) * 2, -1, 1)
current_amount = float(overview.get("amount_billion") or 0)
history_amounts = [float(item.get("amount_billion") or 0) for item in recent_history[:-1] if item.get("amount_billion")]
average_amount = (
float(overview.get("recent_average_amount_billion") or 0)
if "recent_average_amount_billion" in overview
else sum(history_amounts) / len(history_amounts) if history_amounts else current_amount
)
amount_change = _clamp((current_amount / max(average_amount, 1) - 1) * 3, -1, 1)
limit_up = float(overview.get("limit_up_count") or 0)
limit_down = float(overview.get("limit_down_count") or 0)
limit_balance = _clamp((limit_up - limit_down) / max(limit_up + limit_down, 1), -1, 1)
market_inner = _clamp(
(sentiment * 2 - 1) * 0.35
+ (seal_rate * 2 - 1) * 0.2
+ amount_change * 0.2
+ breadth_score * 0.15
+ limit_balance * 0.1,
-1,
1,
)
aggregate = index_context.get("aggregate") or {}
if aggregate:
index_change = _clamp(float(aggregate.get("average_pct_chg") or 0) / 3, -1, 1)
market_outer = index_change
index_evidence = [
f"主要指数平均涨跌 {float(aggregate.get('average_pct_chg') or 0):+.2f}%",
f"主要指数5日平均 {float(aggregate.get('average_return_5d') or 0):+.2f}%(趋势旁证,不参与外显阴阳)",
]
else:
market_outer = _clamp(breadth_score * 0.65 + limit_balance * 0.35, -1, 1)
index_evidence = ["指数接口不可用,以市场宽度和涨跌停结构代替"]
return [
{
"score": stock_inner,
"evidence": [
f"成交额 {stock_amount:.2f} 亿,全市场分位 {amount_rank * 100:.0f}%",
(
f"换手 {float(stock.get('turnover_rate') or 0):.2f}% / 市场 {float(stock.get('market_turnover_rate') or 0):.2f}%"
f"同进度量能 {float(stock.get('volume_activity_ratio') or 0):.2f} 倍"
if stock_intraday
else f"换手率 {float(stock.get('turnover_rate') or 0):.2f}%,开板 {int(stock.get('open_times') or 0)} 次"
),
],
},
{
"score": stock_outer,
"evidence": [
f"{stock.get('name') or '--'}涨跌 {float(stock.get('change') or 0):+.2f}%",
f"状态 {stock.get('status') or '普通'},连板 {int(stock.get('streak') or 0)}",
],
},
{
"score": sector_inner,
"evidence": sector_inner_evidence,
},
{
"score": sector_outer,
"evidence": sector_outer_evidence,
},
{
"score": market_inner,
"evidence": [
f"情绪得分 {float(overview.get('sentiment_score') or 0):.0f},封板率 {float(overview.get('seal_rate') or 0):.1f}%",
f"成交额较近期均值 {amount_change / 3 * 100:+.1f}%,涨跌停 {int(limit_up)}:{int(limit_down)}",
],
},
{
"score": market_outer,
"evidence": index_evidence + [f"上涨 {int(up_count)} 家,下跌 {int(down_count)} 家"],
},
]
def _score_to_line(score: float) -> int:
if score >= 0.72:
return 9
if score >= 0:
return 7
if score <= -0.72:
return 6
return 8
def _momentum_label(score: float) -> str:
if score >= 0.45:
return "势盛而动"
if score >= 0.12:
return "势起未极"
if score > -0.12:
return "阴阳相持"
if score > -0.45:
return "势弱宜察"
return "势衰宜守"
def _current_qi_step(lunar: Any, ymd: str) -> int:
"""按六气分步边界返回当前步次。
本系统约定:六气阶段以大寒为岁首步进,司天在泉随年干支以立春为界切换;
大寒至立春之间,六气已入新一年初之气,司天在泉仍属旧年。
"""
current = int(ymd.replace("-", ""))
table = lunar.getJieQiTable()
boundaries = []
for name in ("大寒", "春分", "小满", "大暑", "秋分", "小雪"):
solar = table.get(name)
if solar is None:
continue
boundaries.append(int(solar.toYmd().replace("-", "")))
if len(boundaries) != 6:
return 1
if current < boundaries[0] or current >= boundaries[5]:
return 6
for index in range(5):
if boundaries[index] <= current < boundaries[index + 1]:
return index + 1
return 6
def _guest_host_relation(host_qi: str, guest_qi: str) -> dict[str, str]:
"""按客气加临主气的五行生克关系给出确定性判定。"""
host_element = QI_ELEMENT[host_qi]
guest_element = QI_ELEMENT[guest_qi]
if guest_element == host_element:
relation = {
"type": "same",
"label": "客主同气",
"order": "同气",
"tendency": "同类之气相并,得势则显,偏盛则亢",
}
elif ELEMENT_GENERATES[guest_element] == host_element:
relation = {
"type": "guest_generates_host",
"label": "客生主",
"order": "相得",
"tendency": "客气生助主气,气机较易相接",
}
elif ELEMENT_GENERATES[host_element] == guest_element:
relation = {
"type": "host_generates_guest",
"label": "主生客",
"order": "相生有泄",
"tendency": "主气生客,时令之力向外流转",
}
elif ELEMENT_CONTROLS[guest_element] == host_element:
relation = {
"type": "guest_controls_host",
"label": "客克主",
"order": "客胜为从",
"tendency": "客气制主,外来变化居于上风",
}
else:
relation = {
"type": "host_controls_guest",
"label": "主克客",
"order": "主胜为逆",
"tendency": "主气制客,时令与来气相持",
}
return {
**relation,
"host_qi": host_qi,
"host_element": host_element,
"guest_qi": guest_qi,
"guest_element": guest_element,
"basis": f"客{guest_element}加临主{host_element}",
}
def _annual_qi_pattern(
movement_element: str,
sitian_element: str,
year_branch: str,
) -> dict[str, Any]:
"""判定中运与岁气的天符、岁会及太乙天符核心格局。"""
is_tianfu = movement_element == sitian_element
is_suihui = (
year_branch in SUIHUI_BRANCHES
and movement_element == BRANCH_ELEMENT[year_branch]
)
names = []
if is_tianfu:
names.append("天符")
if is_suihui:
names.append("岁会")
primary = "太乙天符" if is_tianfu and is_suihui else (names[0] if names else "")
if primary == "太乙天符":
summary = "中运、司天与岁支同气,岁气相合尤著。"
elif primary == "天符":
summary = "中运与司天同气,运气相合。"
elif primary == "岁会":
summary = "中运与岁支五行同气,岁运相会。"
else:
summary = "中运、司天与岁支各循其位。"
return {
"primary": primary,
"names": names,
"is_tianfu": is_tianfu,
"is_suihui": is_suihui,
"summary": summary,
}
def _sector_phase_catalog(overrides: dict[str, str] | None = None) -> list[dict[str, Any]]:
"""返回完整五行行业词表;精确手动归类可移动或新增词条。"""
manual = {
str(name).strip(): element
for name, element in (overrides or {}).items()
if str(name).strip() and element in PHASE_INFO
}
grouped: dict[str, list[dict[str, str]]] = {element: [] for element in PHASE_INFO}
seen: set[str] = set()
for default_element, keywords in SECTOR_PHASE_RULES.items():
for keyword in keywords:
if keyword in seen:
continue
seen.add(keyword)
target = manual.get(keyword, default_element)
grouped[target].append(
{
"name": keyword,
"classification_source": "manual" if keyword in manual else "builtin",
}
)
for name, element in manual.items():
if name in seen:
continue
seen.add(name)
grouped[element].append({"name": name, "classification_source": "manual"})
return [
{
"element": element,
"count": len(grouped[element]),
"industries": grouped[element],
}
for element in PHASE_INFO
]
def _sector_element(name: str, overrides: dict[str, str] | None = None) -> str:
normalized_name = name.strip()
manual_element = (overrides or {}).get(normalized_name)
if manual_element in PHASE_INFO:
return manual_element
best_element = "土"
best_keyword_length = 0
for element, keywords in SECTOR_PHASE_RULES.items():
for keyword in keywords:
if keyword in normalized_name and len(keyword) > best_keyword_length:
best_element = element
best_keyword_length = len(keyword)
return best_element
def _add_phase(weights: dict[str, float], element: str, amount: float) -> None:
weights[element] = weights.get(element, 0) + amount
def _dominant_phase(weights: dict[str, float]) -> str:
return max(weights.items(), key=lambda item: item[1])[0]
def _movement_label(stem: str) -> str:
phase = STEM_MOVEMENT[stem]
tendency = "太过" if stem in YANG_STEMS else "不及"
return f"{MOVEMENT_PAIR[phase]}{phase}{tendency}"
def _phase_distribution(weights: dict[str, float]) -> list[dict[str, Any]]:
total = sum(weights.values()) or 1
return [
{"element": element, "score": score, "percent": round(score / total * 100)}
for element, score in sorted(weights.items(), key=lambda item: item[1], reverse=True)
if score > 0
]
def _percentile(value: float, values: list[float]) -> float:
clean = sorted(item for item in values if math.isfinite(item))
if not clean:
return 0.5
return sum(item <= value for item in clean) / len(clean)
def _clamp(value: float, minimum: float = 0, maximum: float = 1) -> float:
return max(minimum, min(maximum, value))
@lru_cache(maxsize=1)
def _iching_data() -> dict[str, Any]:
payload = json.loads(ICHING_DATA_FILE.read_text(encoding="utf-8"))
data = payload.get("hexagrams") or {}
if len(data) != 64:
raise ValueError("六十四卦经典数据不完整。")
return data