From b3df070481e57df8475840d72f38e9d81bd8a637 Mon Sep 17 00:00:00 2001 From: leefer Date: Fri, 31 Jul 2026 08:57:26 +0800 Subject: [PATCH] migration: preserve heaven trend fortune and heart slice --- app/backend/application.py | 1317 +---------------- app/backend/features/heaven/__init__.py | 24 + app/backend/features/heaven/agent.py | 118 ++ app/backend/features/heaven/engine.py | 1181 +++++++++++++++ app/backend/features/heaven/http.py | 30 + app/backend/features/heaven/repository.py | 111 ++ app/backend/features/heaven/service.py | 1303 ++++++++++++++++ app/database.py | 105 +- app/heaven_agent.py | 119 +- app/heaven_engine.py | 1181 +-------------- app/tests/test_market_mode.py | 4 +- app/tests/test_preservation_slice_heaven.py | 163 ++ docs/migration/evidence/slice-08/README.md | 69 + .../migration/evidence/slice-08/api-diff.json | 71 + .../evidence/slice-08/api-requests.json | 42 + .../evidence/slice-08/database-diff.json | 59 + .../evidence/slice-08/heaven-fortune-1080.png | Bin 0 -> 107431 bytes .../slice-08/heaven-heart-breathing-1080.png | Bin 0 -> 73841 bytes .../slice-08/heaven-heart-dark-1080.png | Bin 0 -> 73004 bytes .../evidence/slice-08/heaven-trend-1080.png | Bin 0 -> 77780 bytes docs/migration/保真迁移状态.json | 10 +- docs/migration/保真迁移账本.md | 15 +- 22 files changed, 3205 insertions(+), 2717 deletions(-) create mode 100644 app/backend/features/heaven/__init__.py create mode 100644 app/backend/features/heaven/agent.py create mode 100644 app/backend/features/heaven/engine.py create mode 100644 app/backend/features/heaven/http.py create mode 100644 app/backend/features/heaven/repository.py create mode 100644 app/backend/features/heaven/service.py create mode 100644 app/tests/test_preservation_slice_heaven.py create mode 100644 docs/migration/evidence/slice-08/README.md create mode 100644 docs/migration/evidence/slice-08/api-diff.json create mode 100644 docs/migration/evidence/slice-08/api-requests.json create mode 100644 docs/migration/evidence/slice-08/database-diff.json create mode 100644 docs/migration/evidence/slice-08/heaven-fortune-1080.png create mode 100644 docs/migration/evidence/slice-08/heaven-heart-breathing-1080.png create mode 100644 docs/migration/evidence/slice-08/heaven-heart-dark-1080.png create mode 100644 docs/migration/evidence/slice-08/heaven-trend-1080.png diff --git a/app/backend/application.py b/app/backend/application.py index 4b968b4..cb4cf6c 100644 --- a/app/backend/application.py +++ b/app/backend/application.py @@ -1,6 +1,5 @@ from __future__ import annotations -import copy import json import re import secrets @@ -21,6 +20,7 @@ from backend.llm import LLMGateway, LLMGatewayError from backend.llm.http import LLMHttpMixin from backend.llm.service import LLMServiceMixin from backend.features.market import ChartDataError, MarketServiceMixin +from backend.features.heaven import HeavenHttpMixin, HeavenServiceMixin, build_personal_field from backend.bootstrap.config import ( DATA_DIR, MENTOR_SKILLS_DIR, @@ -32,15 +32,6 @@ from backend.bootstrap.config import ( validate_text, ) from database import ReviewDatabase -from heaven_agent import HeavenAgentError, interpret_heaven -from heaven_engine import ( - _market_line_scores, - _score_to_line, - build_five_phase_field, - build_market_hexagram, - build_personal_field, - hexagram_from_lines, -) from backend.features.accounts.http import AccountHttpMixin from backend.features.accounts.security import SecretVault from backend.features.accounts.service import AccountService @@ -58,7 +49,7 @@ from backend.features.screener.service import ( from backend.features.sentiment import SentimentServiceMixin from backend.features.system import SystemHttpMixin from backend.features.themes import ThemeServiceMixin -from backend.data.providers.tushare_client import TushareClient, TushareError, _sector_coverage_issue +from backend.data.providers.tushare_client import TushareError LEGACY_SECRET_KEYS = { @@ -87,6 +78,7 @@ class DashboardService( DragonTigerServiceMixin, ScreenerServiceMixin, MentorServiceMixin, + HeavenServiceMixin, LLMServiceMixin, ): def __init__(self) -> None: @@ -713,1283 +705,6 @@ class DashboardService( - @staticmethod - def _heaven_manual_schema(market_mode: str) -> dict[str, dict[str, Any]]: - intraday = market_mode == "intraday" - fields = { - "stock_amount_percentile": {"line": 1, "label": "成交额全市场分位", "unit": "%", "min": 0, "max": 100}, - "stock_turnover_rate": {"line": 1, "label": "个股换手率", "unit": "%", "min": 0, "max": 100}, - "stock_turnover_relative": {"line": 1, "label": "相对市场换手", "unit": "倍", "min": 0, "max": 20}, - "stock_volume_activity_ratio": {"line": 1, "label": "同进度量能", "unit": "倍", "min": 0, "max": 20}, - "stock_seal_amount_million": {"line": 1, "label": "封单金额", "unit": "万元", "min": 0, "max": 100000000}, - "stock_open_times": {"line": 1, "label": "开板次数", "unit": "次", "min": 0, "max": 100, "integer": True}, - "stock_change": {"line": 2, "label": "个股涨跌幅", "unit": "%", "min": -100, "max": 100}, - "stock_streak": {"line": 2, "label": "连板高度", "unit": "板", "min": 0, "max": 100, "integer": True}, - "stock_status": {"line": 2, "label": "个股状态", "type": "select", "options": ["普通", "涨停", "炸板", "跌停"]}, - "sector_name": {"line": [3, 4], "label": "申万二级行业", "type": "text", "max_length": 50}, - "sector_up_count": {"line": 3, "label": "行业上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "sector_down_count": {"line": 3, "label": "行业下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "sector_coverage": {"line": 3, "label": "成分行情覆盖率", "unit": "%", "min": 0, "max": 100}, - "sector_relative_turnover": {"line": 3, "label": "行业相对市场换手", "unit": "倍", "min": 0, "max": 20}, - "sector_member_equal_change": {"line": 3, "label": "成分等权涨跌幅", "unit": "%", "min": -100, "max": 100}, - "sector_change": {"line": 4, "label": "申万官方涨跌幅", "unit": "%", "min": -100, "max": 100}, - "sector_leading_pct": {"line": [3, 4], "label": "行业领涨股涨跌幅", "unit": "%", "min": -100, "max": 100}, - "market_sentiment_score": {"line": 5, "label": "市场情绪温度", "unit": "分", "min": 0, "max": 100}, - "market_seal_rate": {"line": 5, "label": "封板率", "unit": "%", "min": 0, "max": 100}, - "market_amount_billion": {"line": 5, "label": "两市成交额", "unit": "亿元", "min": 0, "max": 10000000}, - "market_recent_average_amount_billion": {"line": 5, "label": "近期平均成交额", "unit": "亿元", "min": 0, "max": 10000000}, - "market_up_count": {"line": 5, "label": "上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "market_down_count": {"line": 5, "label": "下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "market_limit_up_count": {"line": 5, "label": "涨停家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "market_limit_down_count": {"line": 5, "label": "跌停家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, - "index_sh_change": {"line": 6, "label": "上证指数涨跌幅", "unit": "%", "min": -20, "max": 20}, - "index_sz_change": {"line": 6, "label": "深证成指涨跌幅", "unit": "%", "min": -20, "max": 20}, - "index_cy_change": {"line": 6, "label": "创业板指涨跌幅", "unit": "%", "min": -20, "max": 20}, - "note": {"line": [], "label": "补录说明", "type": "text", "max_length": 200}, - } - if intraday: - for key in ("stock_seal_amount_million", "stock_open_times"): - fields.pop(key) - else: - for key in ("stock_turnover_relative", "stock_volume_activity_ratio", "sector_relative_turnover"): - fields.pop(key) - return fields - - @classmethod - def _validate_heaven_manual_data( - cls, raw: Any, market_mode: str - ) -> dict[str, Any]: - if raw in (None, ""): - return {} - if not isinstance(raw, dict): - raise ValueError("六爻补录数据格式不正确。") - schema = cls._heaven_manual_schema(market_mode) - unknown = set(raw) - set(schema) - if unknown: - raise ValueError(f"六爻补录包含未知字段:{next(iter(sorted(unknown)))}") - values: dict[str, Any] = {} - for key, value in raw.items(): - if value is None or (isinstance(value, str) and not value.strip()): - continue - spec = schema[key] - if spec.get("type") == "text": - values[key] = validate_text(value, spec["label"], int(spec["max_length"])) - continue - if spec.get("type") == "select": - text = str(value).strip() - if text not in spec["options"]: - raise ValueError(f"{spec['label']}不在允许范围内。") - values[key] = text - continue - try: - number = float(value) - except (TypeError, ValueError) as exc: - raise ValueError(f"{spec['label']}必须是数字。") from exc - if number < float(spec["min"]) or number > float(spec["max"]): - raise ValueError( - f"{spec['label']}应在 {spec['min']} 至 {spec['max']} 之间。" - ) - values[key] = int(number) if spec.get("integer") else number - return values - - @staticmethod - def _apply_heaven_manual_data( - dashboard: dict[str, Any], - index_context: dict[str, Any], - sector: dict[str, Any] | None, - stock: dict[str, Any] | None, - manual_data: dict[str, Any], - market_mode: str, - trade_date: str, - stock_code: str, - ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]: - dashboard = copy.deepcopy(dashboard) - index_context = copy.deepcopy(index_context or {}) - sector = copy.deepcopy(sector or {}) - stock = copy.deepcopy(stock or {}) - overview = dashboard.setdefault("overview", {}) - - stock_map = { - "stock_amount_percentile": "amount_percentile", - "stock_turnover_rate": "turnover_rate", - "stock_turnover_relative": "turnover_relative", - "stock_volume_activity_ratio": "volume_activity_ratio", - "stock_seal_amount_million": "seal_amount_million", - "stock_open_times": "open_times", - "stock_change": "change", - "stock_streak": "streak", - "stock_status": "status", - } - sector_map = { - "sector_name": "name", - "sector_up_count": "up_count", - "sector_down_count": "down_count", - "sector_coverage": "coverage", - "sector_relative_turnover": "relative_turnover", - "sector_member_equal_change": "member_equal_change", - "sector_change": "change", - "sector_leading_pct": "leading_pct", - } - overview_map = { - "market_sentiment_score": "sentiment_score", - "market_seal_rate": "seal_rate", - "market_amount_billion": "amount_billion", - "market_recent_average_amount_billion": "recent_average_amount_billion", - "market_up_count": "up_count", - "market_down_count": "down_count", - "market_limit_up_count": "limit_up_count", - "market_limit_down_count": "limit_down_count", - } - for manual_key, target in stock_map.items(): - if manual_key in manual_data: - stock[target] = manual_data[manual_key] - for manual_key, target in sector_map.items(): - if manual_key in manual_data: - sector[target] = manual_data[manual_key] - for manual_key, target in overview_map.items(): - if manual_key in manual_data: - overview[target] = manual_data[manual_key] - - if any(key.startswith("stock_") for key in manual_data): - stock.setdefault("code", stock_code) - stock.setdefault("name", stock_code or "--") - stock["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical" - if market_mode == "intraday" and "stock_volume_activity_ratio" in manual_data: - stock["activity_source"] = "user_supplied" - if any(key.startswith("sector_") for key in manual_data): - sector["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical" - sector.setdefault("taxonomy", "sw_l2") - - index_keys = ( - ("index_sh_change", "000001.SH", "上证指数"), - ("index_sz_change", "399001.SZ", "深证成指"), - ("index_cy_change", "399006.SZ", "创业板指"), - ) - rows = {str(row.get("ts_code") or row.get("code") or ""): dict(row) for row in index_context.get("indices") or []} - for manual_key, code, name in index_keys: - if manual_key not in manual_data: - continue - row = rows.get(code, {"ts_code": code, "name": name}) - row.update({"pct_chg": manual_data[manual_key], "trade_date": trade_date}) - rows[code] = row - ordered_rows = [rows.get(code) for _, code, _ in index_keys] - if all(ordered_rows): - index_context["indices"] = ordered_rows - changes = [float(row.get("pct_chg") or 0) for row in ordered_rows] - aggregate = dict(index_context.get("aggregate") or {}) - aggregate["average_pct_chg"] = sum(changes) / 3 - index_context["aggregate"] = aggregate - return dashboard, index_context, sector, stock - - @classmethod - def _heaven_line_checks( - cls, - trade_date: str, - dashboard: dict[str, Any], - recent_history: list[dict[str, Any]], - index_context: dict[str, Any], - sector: dict[str, Any], - stock: dict[str, Any], - market_mode: str, - manual_data: dict[str, Any], - ) -> list[dict[str, Any]]: - intraday = market_mode == "intraday" - closed = market_mode == "closed" - schema = cls._heaven_manual_schema(market_mode) - required = { - 1: (["stock_amount_percentile", "stock_turnover_relative", "stock_volume_activity_ratio"] if intraday else ["stock_amount_percentile", "stock_turnover_rate", "stock_seal_amount_million", "stock_open_times"]), - 2: ["stock_change", "stock_streak", "stock_status"], - 3: (["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_relative_turnover"] if intraday else ["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_member_equal_change", "sector_leading_pct"]), - 4: ["sector_name", "sector_change", "sector_leading_pct"], - 5: ["market_sentiment_score", "market_seal_rate", "market_amount_billion", "market_recent_average_amount_billion", "market_up_count", "market_down_count", "market_limit_up_count", "market_limit_down_count"], - 6: ["index_sh_change", "index_sz_change", "index_cy_change"], - } - names = { - 1: ("初爻", "个股内核", "成交活跃、换手与量能"), - 2: ("二爻", "个股外显", "涨跌、连板与状态"), - 3: ("三爻", "行业内核", "行业宽度与成交活跃"), - 4: ("四爻", "行业外显", "行业涨跌与领涨表现"), - 5: ("五爻", "市场内核", "情绪、封板、成交与市场宽度"), - 6: ("上爻", "指数外显", "三大指数当日涨跌"), - } - - index_date = str(index_context.get("trade_date") or "").replace("-", "") - index_rows = list(index_context.get("indices") or []) - index_dates = {str(row.get("trade_date") or "").replace("-", "") for row in index_rows} - index_issues = [] - if len(index_rows) < 3: - index_issues.append(f"三大指数仅取得 {len(index_rows)}/3 条行情") - elif index_date != trade_date or index_dates != {trade_date}: - actual_dates = "、".join(sorted(value for value in index_dates if value)) or "未知" - index_issues.append(f"指数实际日期为 {actual_dates},目标交易日为 {trade_date}") - elif not index_context.get("precise"): - index_issues.append("三大指数行情未通过完整性校验") - elif intraday and not index_context.get("realtime"): - index_issues.append("盘中缺少可核验的实时指数行情") - elif not intraday and (index_context.get("realtime") or str(index_context.get("source") or "") != "tushare"): - index_issues.append("收盘或历史行情不是官方指数日线") - - sector_date = str(sector.get("trade_date") or "").replace("-", "") - sector_coverage = float(sector.get("coverage") or 0) - sector_explained_count = int( - sector.get("explained_count") - if sector.get("explained_count") is not None - else sector.get("quote_count") or 0 - ) - sector_explained_coverage = float( - sector.get("explained_coverage") - if sector.get("explained_coverage") is not None - else sector_coverage - ) - sector_coverage_issue = _sector_coverage_issue( - int(sector.get("member_count") or 0), - int(sector.get("quote_count") or 0), - sector_explained_coverage, - sector_explained_count, - ) - sector_common = [] - if not sector: - sector_common.append("未取得申万二级行业归属") - elif sector.get("taxonomy") != "sw_l2": - sector_common.append("行业分类不是申万二级") - elif sector_date != trade_date: - sector_common.append("行业行情日期与目标交易日不一致") - elif intraday and not sector.get("realtime"): - sector_common.append("盘中行业行情不是申万实时行情") - elif market_mode == "historical" and sector.get("realtime"): - sector_common.append("历史行业行情不能使用实时快照") - elif closed and sector.get("realtime") and not sector.get("finalized"): - sector_common.append("收盘行业实时行情尚未形成15:00最终快照") - sector_inner = list(sector_common) - sector_outer = list(sector_common) - if not sector.get("inner_precise", sector.get("precise")): - sector_inner.append(str(sector.get("inner_error") or sector.get("error") or "行业内核数据未通过校验")) - if not sector.get("outer_precise", sector.get("precise")): - sector_outer.append(str(sector.get("outer_error") or sector.get("error") or "行业外显数据未通过校验")) - if sector and sector_coverage_issue and sector_coverage_issue not in sector_inner: - sector_inner.append(sector_coverage_issue) - if sector.get("realtime") and not sector.get("relative_turnover"): - sector_inner.append("缺少行业相对全市场换手活跃度") - - stock_date = str(stock.get("trade_date") or "").replace("-", "") - stock_common = [] - if not stock.get("code"): - stock_common.append("尚未载入有效个股") - elif stock_date != trade_date: - stock_common.append(f"个股实际日期为 {stock_date or '未知'},目标交易日为 {trade_date}") - elif not stock.get("precise"): - stock_common.append("个股行情未通过完整性校验") - elif intraday and not stock.get("realtime"): - stock_common.append("盘中个股行情不是实时行情") - elif not intraday and (stock.get("realtime") or str(stock.get("data_source") or "") != "tushare"): - stock_common.append("收盘或历史个股行情不是官方日线") - stock_inner = list(stock_common) - if intraday and stock.get("turnover_source") in {None, "", "unavailable"}: - stock_inner.append("缺少可核验的实时换手率") - if intraday and stock.get("activity_source") in {None, "", "unavailable"}: - stock_inner.append("缺少同时间进度量能基准") - - overview = dashboard.get("overview") or {} - market_key_map = { - "market_sentiment_score": "sentiment_score", "market_seal_rate": "seal_rate", - "market_amount_billion": "amount_billion", "market_recent_average_amount_billion": "recent_average_amount_billion", - "market_up_count": "up_count", "market_down_count": "down_count", - "market_limit_up_count": "limit_up_count", "market_limit_down_count": "limit_down_count", - } - market_issues = [] - for manual_key, source_key in market_key_map.items(): - if source_key == "recent_average_amount_billion": - history_values = [item.get("amount_billion") for item in recent_history[:-1] if item.get("amount_billion") is not None] - if source_key not in overview and not history_values: - market_issues.append(f"缺少{schema[manual_key]['label']}") - elif source_key not in overview or overview.get(source_key) is None: - market_issues.append(f"缺少{schema[manual_key]['label']}") - - automatic_issues = { - 1: stock_inner, 2: stock_common, 3: sector_inner, - 4: sector_outer, 5: market_issues, 6: index_issues, - } - limits = list(dashboard.get("limits") or []) - scores = _market_line_scores(dashboard, recent_history, index_context, sector, stock, limits) - - value_map: dict[str, Any] = { - "stock_amount_percentile": stock.get("amount_percentile"), - "stock_turnover_rate": stock.get("turnover_rate"), - "stock_turnover_relative": stock.get("turnover_relative"), - "stock_volume_activity_ratio": stock.get("volume_activity_ratio"), - "stock_seal_amount_million": stock.get("seal_amount_million"), - "stock_open_times": stock.get("open_times"), - "stock_change": stock.get("change"), "stock_streak": stock.get("streak"), - "stock_status": stock.get("status"), "sector_name": sector.get("name"), - "sector_up_count": sector.get("up_count"), "sector_down_count": sector.get("down_count"), - "sector_coverage": sector.get("coverage"), "sector_relative_turnover": sector.get("relative_turnover"), - "sector_member_equal_change": sector.get("member_equal_change"), - "sector_change": sector.get("change"), "sector_leading_pct": sector.get("leading_pct"), - "market_sentiment_score": overview.get("sentiment_score"), "market_seal_rate": overview.get("seal_rate"), - "market_amount_billion": overview.get("amount_billion"), - "market_recent_average_amount_billion": overview.get("recent_average_amount_billion"), - "market_up_count": overview.get("up_count"), "market_down_count": overview.get("down_count"), - "market_limit_up_count": overview.get("limit_up_count"), "market_limit_down_count": overview.get("limit_down_count"), - } - history_values = [float(item.get("amount_billion")) for item in recent_history[:-1] if item.get("amount_billion") is not None] - if value_map["market_recent_average_amount_billion"] is None and history_values: - value_map["market_recent_average_amount_billion"] = sum(history_values) / len(history_values) - if value_map["stock_amount_percentile"] is None and not intraday: - amount = float(stock.get("amount_billion") or 0) - amounts = [float(item.get("amount_billion") or 0) for item in limits if item.get("amount_billion") is not None] - value_map["stock_amount_percentile"] = ( - sum(item <= amount for item in amounts) / len(amounts) * 100 if amounts else None - ) - row_by_code = {str(row.get("ts_code") or row.get("code") or ""): row for row in index_context.get("indices") or []} - value_map.update({ - "index_sh_change": (row_by_code.get("000001.SH") or {}).get("pct_chg"), - "index_sz_change": (row_by_code.get("399001.SZ") or {}).get("pct_chg"), - "index_cy_change": (row_by_code.get("399006.SZ") or {}).get("pct_chg"), - }) - - def missing_value(key: str) -> bool: - value = value_map.get(key) - return value is None or (isinstance(value, str) and not value.strip()) - - invalid_fields = { - line_number: {key for key in keys if missing_value(key)} - for line_number, keys in required.items() - } - if stock_common: - invalid_fields[1].update(required[1]) - invalid_fields[2].update(required[2]) - else: - if intraday and stock.get("turnover_source") in {None, "", "unavailable"}: - invalid_fields[1].add("stock_turnover_relative") - if intraday and stock.get("activity_source") in {None, "", "unavailable"}: - invalid_fields[1].add("stock_volume_activity_ratio") - - if sector_common: - invalid_fields[3].update(required[3]) - invalid_fields[4].update(required[4]) - else: - if not sector.get("inner_precise", sector.get("precise")) or sector_coverage_issue: - invalid_fields[3].update(key for key in required[3] if key != "sector_name") - if sector.get("realtime") and not sector.get("relative_turnover"): - invalid_fields[3].add("sector_relative_turnover") - # The official SW index supplies only the sector's external change. A valid - # membership name and member-stock leader remain usable when that quote fails. - if not sector.get("outer_precise", sector.get("precise")): - invalid_fields[4].add("sector_change") - - if index_issues: - invalid_fields[6].update(required[6]) - - checks = [] - for line_number in range(1, 7): - manual_keys = [key for key in required[line_number] if key in manual_data] - unresolved_fields = [ - key for key in required[line_number] - if key in invalid_fields[line_number] and key not in manual_data - ] - hard_missing_identity = line_number in {1, 2} and not stock.get("code") - passed = not hard_missing_identity and not unresolved_fields - status = "manual" if passed and manual_keys else "passed" if passed else "failed" - reasons = [] if passed else [ - *( ["请先输入并载入股票代码或名称"] if hard_missing_identity else automatic_issues[line_number] ), - *( ["需补充:" + "、".join(schema[key]["label"] for key in unresolved_fields)] if unresolved_fields else [] ), - ] - score = float(scores[line_number - 1]["score"]) - position, layer, formula = names[line_number] - checks.append({ - "line": line_number, "position": position, "layer": layer, "formula": formula, - "status": status, "passed": passed, "reasons": reasons, - "score": round(score, 3) if passed else None, - "line_value": _score_to_line(score) if passed else None, - "evidence": scores[line_number - 1]["evidence"] if passed else [], - "fields": [ - { - "key": key, "label": schema[key]["label"], "unit": schema[key].get("unit", ""), - "type": schema[key].get("type", "number"), "options": schema[key].get("options", []), - "value": value_map.get(key), "manual": key in manual_data, - "required": True, "min": schema[key].get("min"), "max": schema[key].get("max"), - "integer": bool(schema[key].get("integer")), - } - for key in required[line_number] - ], - }) - return checks - - def _resolve_heaven_stock_code(self, query: str) -> str: - raw = validate_text(query, "股票代码或名称", 30, required=True) - code_match = re.fullmatch(r"(\d{6})(?:\.(?:SH|SZ|BJ))?", raw.upper()) - if code_match: - return validate_stock_code(code_match.group(1)) - - candidates = self.database.search_stock_master(raw) - exact = [item for item in candidates if str(item.get("name") or "").casefold() == raw.casefold()] - if not exact and self.configured: - try: - rows = self._tushare_client().query( - "stock_basic", - {"name": raw, "list_status": "L"}, - "ts_code,symbol,name,industry,market,list_date", - ) - except TushareError: - rows = [] - if rows: - self.database.upsert_stock_master(rows) - candidates = self.database.search_stock_master(raw) - exact = [ - item - for item in candidates - if str(item.get("name") or "").casefold() == raw.casefold() - ] - - matches = exact or candidates - if len(matches) == 1: - return validate_stock_code(str(matches[0].get("code") or "")) - if len(matches) > 1: - choices = "、".join( - f"{item.get('name') or '--'}({item.get('code') or '--'})" - for item in matches[:5] - ) - raise ValueError(f"匹配到多只股票:{choices}。请输入六位股票代码。") - raise ValueError(f"未找到股票“{raw}”,请检查名称或输入六位股票代码。") - - def heaven_setup( - self, - trade_date: str, - sector_name: str = "", - stock_code: str = "", - manual_data: dict[str, Any] | None = None, - ) -> dict[str, Any]: - normalized_date = normalize_date(trade_date) - dashboard = self.get_dashboard(normalized_date) - data_date = normalize_date(str(dashboard.get("meta", {}).get("trade_date") or normalized_date)) - recent_history = self.database.snapshot_summaries(data_date, 10) - market_mode = self._heaven_market_mode(data_date, dashboard) - manual_data = self._validate_heaven_manual_data(manual_data, market_mode) - index_context = self._heaven_index_context(data_date, dashboard, market_mode) - external_stock = None - normalized_stock_code = "" - if stock_code.strip(): - normalized_stock_code = self._resolve_heaven_stock_code(stock_code) - external_stock = self._heaven_stock_context( - normalized_stock_code, - data_date, - dashboard, - market_mode, - ) - external_sector = None - if normalized_stock_code and self.configured: - external_sector = self._heaven_sector_context( - normalized_stock_code, - data_date, - market_mode, - ) - if external_sector and external_stock: - external_stock["sector"] = external_sector.get("name") or external_stock.get("sector") - dashboard, index_context, external_sector, external_stock = self._apply_heaven_manual_data( - dashboard, - index_context, - external_sector, - external_stock, - manual_data, - market_mode, - data_date, - normalized_stock_code, - ) - if external_sector and external_stock: - external_stock["sector"] = external_sector.get("name") or external_stock.get("sector") - sector_input = str((external_sector or {}).get("name") or sector_name.strip()) - if not normalized_stock_code: - data_checks = [] - chart = { - "available": False, - "selection_required": True, - "data_trade_date": data_date, - "sector": "", - "sector_code": "", - "sector_taxonomy": "", - "stock": {"code": "", "name": "", "status": ""}, - "quality": { - "status": "awaiting_selection", - "issues": [], - "principle": "", - "sources": [], - }, - "index_context": index_context, - } - else: - data_checks = self._heaven_line_checks( - data_date, - dashboard, - recent_history, - index_context, - external_sector or {}, - external_stock or {}, - market_mode, - manual_data, - ) - quality_issues = [ - f"{check['position']}·{check['layer']}:{';'.join(check['reasons'])}" - for check in data_checks - if not check["passed"] - ] - if quality_issues: - chart = { - "available": False, - "selection_required": False, - "data_trade_date": data_date, - "sector": str((external_sector or {}).get("name") or sector_input or "--"), - "sector_code": str((external_sector or {}).get("code") or ""), - "sector_taxonomy": str((external_sector or {}).get("taxonomy") or ""), - "stock": { - "code": normalized_stock_code, - "name": str((external_stock or {}).get("name") or "--"), - "status": str((external_stock or {}).get("status") or ""), - }, - "quality": { - "status": "blocked", - "issues": quality_issues, - "principle": "六爻任一层缺少同日、同口径的有效数据,本系统不成卦。", - "sources": self._heaven_trend_sources( - data_date, index_context, external_sector, external_stock - ), - }, - "index_context": index_context, - } - else: - chart = build_market_hexagram( - dashboard, - recent_history, - index_context, - sector_input, - normalized_stock_code, - external_stock, - external_sector, - ) - chart["available"] = True - chart["selection_required"] = False - manual_active = any(check["status"] == "manual" for check in data_checks) - chart["quality"] = { - "status": "manual" if manual_active else "verified", - "issues": [], - "principle": ( - "自动行情与用户补充数据均已通过同一套量化公式校验。" - if manual_active - else "指数、板块、个股均已通过同日同口径校验。" - ), - "sources": [ - *self._heaven_trend_sources( - data_date, index_context, external_sector, external_stock - ), - *([{ - "lines": "补录爻位", - "layer": "用户补充", - "realtime": market_mode == "intraday", - "detail": str(manual_data.get("note") or "量化数据经原公式重新计算"), - }] if manual_active else []), - ], - } - chart["data_checks"] = data_checks - chart["manual_data"] = manual_data - sector_phase_overrides = self.database.list_sector_phase_overrides() - field = build_five_phase_field( - normalized_date, - sector_phase_overrides, - ) - personal_profile = self.account_personal_field( - normalized_date, - field, - public=True, - ) - daily_fortune_reading = self.database.latest_heaven_reading( - self.current_user_id, "fortune", normalized_date - ) - if self._legacy_truncated_heaven_reading(daily_fortune_reading): - daily_fortune_reading = None - return { - "trade_date": data_date, - "calendar_date": normalized_date, - "market_mode": market_mode, - "chart": chart, - "field": field, - "personal_profile": personal_profile, - "daily_fortune_reading": daily_fortune_reading, - "sector_phase_overrides": [ - {"name": name, "element": element} - for name, element in sector_phase_overrides.items() - ], - "llm": { - "configured": self.llm_configured, - "model": self.llm_primary_model if self.llm_configured else "", - "fallback_configured": self.llm_fallback_configured, - "fallback_model": self.llm_fallback_model if self.llm_fallback_configured else "", - }, - } - - def _heaven_stock_context( - self, - stock_code: str, - trade_date: str, - dashboard: dict[str, Any], - market_mode: str, - ) -> dict[str, Any]: - """Return the only stock contract accepted by heaven trend.""" - pool_row = next( - ( - dict(row) for key in ("limits", "broken", "down_limits") - for row in dashboard.get(key) or [] - if str(row.get("code") or "") == stock_code - ), - {}, - ) - if market_mode == "intraday": - if self.configured: - try: - quote = self._tushare_client().realtime_stock_quote( - tushare_code(stock_code), - trade_date, - ) - return { - **quote, - "status": pool_row.get("status") or "普通", - "seal_amount_million": pool_row.get("seal_amount_million") or 0, - "open_times": pool_row.get("open_times") or 0, - "streak": pool_row.get("streak") or 0, - "precise": True, - } - except TushareError: - pass - if pool_row: - return { - **pool_row, - "data_source": "dashboard_rt" if dashboard.get("meta", {}).get("realtime") else "dashboard", - "trade_date": trade_date, - "realtime": bool(dashboard.get("meta", {}).get("realtime")), - "precise": False, - } - return { - "code": stock_code, - "name": "--", - "sector": "其他", - "trade_date": trade_date, - "realtime": False, - "precise": False, - } - - detail = self.get_stock_detail(stock_code, trade_date, force=True) - detail_meta = detail.get("meta") or {} - stock = detail.get("stock") or {} - resolved_date = normalize_date(str(detail_meta.get("trade_date") or trade_date)) - source = str(detail_meta.get("source") or "") - return { - "code": stock_code, - "name": stock.get("name") or pool_row.get("name") or "--", - "sector": stock.get("industry") or pool_row.get("sector") or "其他", - "status": pool_row.get("status") or "普通", - "change": stock.get("change") or 0, - "turnover_rate": stock.get("turnover_rate") or 0, - "amount_billion": stock.get("amount_billion") or 0, - "seal_amount_million": pool_row.get("seal_amount_million") or 0, - "open_times": pool_row.get("open_times") or 0, - "streak": pool_row.get("streak") or 0, - "data_source": source, - "trade_date": resolved_date, - "realtime": False, - "precise": source == "tushare" and resolved_date == trade_date, - } - - @staticmethod - def _heaven_market_mode( - trade_date: str, - dashboard: dict[str, Any], - now: datetime | None = None, - ) -> str: - """区分盘中、今日收盘和历史,避免把 rt_k 数据来源误当成交易状态。""" - now = now or datetime.now().astimezone() - if trade_date != now.strftime("%Y%m%d"): - return "historical" - meta = dashboard.get("meta") or {} - status = str(meta.get("market_status") or "").lower() - local_time = now.time().replace(tzinfo=None) - if status == "closed" or local_time > datetime.strptime("15:05", "%H:%M").time(): - return "closed" - if status in {"trading", "auction", "pre_open"} or ( - bool(meta.get("realtime")) - and local_time >= datetime.strptime("09:15", "%H:%M").time() - ): - return "intraday" - return "historical" - - @staticmethod - def _heaven_trend_sources( - trade_date: str, - index_context: dict[str, Any], - sector: dict[str, Any] | None, - stock: dict[str, Any] | None, - ) -> list[dict[str, Any]]: - sector = sector or {} - stock = stock or {} - return [ - { - "lines": "五爻、上爻", - "layer": "指数", - "source": index_context.get("source") or "unavailable", - "trade_date": index_context.get("trade_date") or "", - "realtime": bool(index_context.get("realtime")), - "detail": f"三大指数 {len(index_context.get('indices') or [])}/3", - }, - { - "lines": "三爻、四爻", - "layer": "行业", - "source": sector.get("source") or "unavailable", - "trade_date": sector.get("trade_date") or "", - "realtime": bool(sector.get("realtime")), - "detail": ( - f"申万二级 {sector.get('name') or '--'} {sector.get('code') or '--'} " - f"成分覆盖 {int(sector.get('quote_count') or 0)}/{int(sector.get('member_count') or 0)}" - ), - }, - { - "lines": "初爻、二爻", - "layer": "个股", - "source": stock.get("data_source") or "unavailable", - "trade_date": stock.get("trade_date") or trade_date, - "realtime": bool(stock.get("realtime")), - "detail": ( - f"{stock.get('name') or '--'};换手基准 " - f"{stock.get('capital_trade_date') or '--'}" - ), - }, - ] - - @staticmethod - def _heaven_trend_quality_issues( - trade_date: str, - dashboard: dict[str, Any], - index_context: dict[str, Any], - sector: dict[str, Any] | None, - stock: dict[str, Any] | None, - market_mode: str = "historical", - ) -> list[str]: - issues: list[str] = [] - intraday = market_mode == "intraday" - closed = market_mode == "closed" - if intraday: - meta = dashboard.get("meta") or {} - market_status = str(meta.get("market_status") or "") - now = datetime.now().astimezone() - try: - updated_at = datetime.fromisoformat(str(meta.get("updated_at") or "")) - if updated_at.tzinfo is None: - updated_at = updated_at.replace(tzinfo=now.tzinfo) - snapshot_age = (now - updated_at.astimezone(now.tzinfo)).total_seconds() - except ValueError: - snapshot_age = float("inf") - if market_status in {"trading", "auction", "pre_open"} and snapshot_age > 120: - issues.append("主行情快照超过2分钟,请点击顶部刷新") - # 收盘后不再用 dashboard.market_status 作为阻断条件。盘后同步可能将 - # rt_k 快照替换成同日盘后日线而不带该字段;六爻数据本身的日期、 - # 完整性和来源校验已足以判断是否可以成卦。 - - index_date = str(index_context.get("trade_date") or "").replace("-", "") - index_rows = list(index_context.get("indices") or []) - index_row_dates = { - str(row.get("trade_date") or "").replace("-", "") for row in index_rows - } - if not index_context.get("precise") or len(index_rows) < 3: - issues.append("指数层缺少三大指数的有效行情") - elif index_date != trade_date or index_row_dates != {trade_date}: - issues.append("指数行情与目标交易日不一致") - elif intraday and not index_context.get("realtime"): - issues.append("盘中指数层缺少可核验的实时行情") - elif not intraday and ( - index_context.get("realtime") - or str(index_context.get("source") or "") != "tushare" - ): - issues.append("历史/收盘指数层必须使用 Tushare 官方指数日线") - - sector = sector or {} - sector_date = str(sector.get("trade_date") or "").replace("-", "") - sector_coverage = float(sector.get("coverage") or 0) - sector_explained_count = int( - sector.get("explained_count") - if sector.get("explained_count") is not None - else sector.get("quote_count") or 0 - ) - sector_explained_coverage = float( - sector.get("explained_coverage") - if sector.get("explained_coverage") is not None - else sector_coverage - ) - sector_coverage_issue = _sector_coverage_issue( - int(sector.get("member_count") or 0), - int(sector.get("quote_count") or 0), - sector_explained_coverage, - sector_explained_count, - ) - if not sector: - issues.append("行业层缺少申万二级行业归属") - elif sector.get("taxonomy") != "sw_l2": - issues.append("行业层必须使用申万二级行业分类") - elif sector_date != trade_date: - issues.append("行业行情与目标交易日不一致") - elif intraday and not sector.get("realtime"): - issues.append("盘中行业层缺少申万实时行情") - elif market_mode == "historical" and sector.get("realtime"): - issues.append("历史行业层不能使用实时快照") - elif closed and sector.get("realtime") and not sector.get("finalized"): - issues.append("收盘行业层缺少15:00最终快照") - if not sector.get("inner_precise", sector.get("precise")): - issues.append("行业内核缺少可核验的成分行情") - if not sector.get("outer_precise", sector.get("precise")): - issues.append("行业外显缺少申万官方行情") - if sector and sector_coverage_issue: - issues.append(sector_coverage_issue) - if sector.get("realtime") and not sector.get("relative_turnover"): - issues.append("行业内核缺少相对全市场换手活跃度") - - stock = stock or {} - stock_date = str(stock.get("trade_date") or "").replace("-", "") - if not stock or not stock.get("code"): - issues.append("个股层尚未载入有效标的") - elif not stock.get("precise"): - issues.append("个股层缺少可核验的行情数据") - elif stock_date != trade_date: - issues.append("个股行情与目标交易日不一致") - elif intraday and not stock.get("realtime"): - issues.append("盘中个股层不是 rt_k 实时行情") - elif not intraday and ( - stock.get("realtime") - or str(stock.get("data_source") or "") != "tushare" - ): - issues.append("历史/收盘个股层必须使用 Tushare 官方日线") - if intraday and stock and not stock.get("turnover_source"): - issues.append("个股内核缺少可核验的实时换手率") - elif intraday and stock.get("turnover_source") == "unavailable": - issues.append("个股内核缺少流通股本,无法计算实时换手率") - if intraday and stock.get("activity_source") == "unavailable": - issues.append("个股内核缺少近5日量能基准") - elif intraday and not stock.get("activity_source"): - issues.append("个股内核缺少同时间进度量能") - return issues - - def heaven_personal(self, payload: dict[str, Any]) -> dict[str, Any]: - trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) - field = build_five_phase_field( - trade_date, - self.database.list_sector_phase_overrides(), - ) - personal = self.account_personal_field(trade_date, field, public=True) - if not personal: - raise ValueError("请先在账号设置中保存个人命理资料。") - return personal - - def heaven_hexagram(self, raw_lines: Any) -> dict[str, Any]: - if not isinstance(raw_lines, list): - raise ValueError("六爻起卦结果格式不正确。") - try: - lines = [int(value) for value in raw_lines] - except (TypeError, ValueError) as exc: - raise ValueError("六爻必须由六、七、八、九组成。") from exc - return hexagram_from_lines(lines) - - def heaven_readings( - self, mode: str, context_date: str = "", limit: int = 100 - ) -> dict[str, Any]: - mode = str(mode or "").strip() - if mode not in {"trend", "fortune", "heart"}: - raise ValueError("解读记录类型不正确。") - normalized_date = normalize_date(context_date) if context_date else "" - return { - "mode": mode, - "items": self.database.list_heaven_readings( - self.current_user_id, mode, normalized_date, limit - ), - } - - @staticmethod - def _heaven_reading_identity( - mode: str, context_date: str, context: dict[str, Any] - ) -> tuple[str, str]: - display_date = DashboardService._display_compact_date(context_date) - if mode == "trend": - stock = (context.get("selected_focus") or {}).get("stock") or {} - code = str(stock.get("code") or "").strip() - name = str(stock.get("name") or "").strip() - hexagram = context.get("hexagram") or {} - transformed = hexagram.get("transformed") or {} - subject = " ".join(item for item in (code, name) if item) or "观势" - detail = f"{display_date} · {hexagram.get('name') or '--'} → {transformed.get('name') or '--'}" - return subject, detail - if mode == "fortune": - field = context.get("five_phase_field") or {} - pillars = field.get("pillars") or {} - dominant = (field.get("balance") or [{}])[0] - subject = f"{display_date} 观气" - detail = ( - f"{pillars.get('year') or '--'}年 · {pillars.get('month') or '--'}月 · " - f"{pillars.get('day') or '--'}日 · {dominant.get('element') or '--'}气偏显" - ) - return subject, detail - hexagram = context.get("hexagram") or {} - transformed = hexagram.get("transformed") or {} - return ( - f"{display_date} 观心", - f"{hexagram.get('name') or '--'} → {transformed.get('name') or '--'}", - ) - - def heaven_interpret(self, payload: dict[str, Any]) -> dict[str, Any]: - mode = str(payload.get("mode") or "").strip() - if mode not in {"trend", "fortune", "heart"}: - raise ValueError("问天解读模式不正确。") - trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) - if mode == "fortune": - existing = self.database.latest_heaven_reading( - self.current_user_id, "fortune", trade_date - ) - if self._legacy_truncated_heaven_reading(existing): - self.database.delete_heaven_reading( - self.current_user_id, int(existing["id"]) - ) - existing = None - if existing: - return { - "answer": existing["answer"], - "mode": mode, - "compiler": "stored", - "notice": "", - "reading": existing, - "reused": True, - } - if mode in {"trend", "fortune"}: - setup = self.heaven_setup( - trade_date, - str(payload.get("sector") or ""), - str(payload.get("stock_code") or ""), - payload.get("manual_data"), - ) - if mode == "trend": - chart = setup["chart"] - if not chart.get("available"): - issues = ";".join((chart.get("quality") or {}).get("issues") or []) - raise ValueError(f"观势数据未通过六爻校验,暂不解势:{issues}") - hexagram_context = json.loads(json.dumps(chart["hexagram"], ensure_ascii=False)) - for line in hexagram_context.get("lines", []): - line.pop("evidence", None) - line.pop("score", None) - line.pop("talent", None) - line.pop("layer", None) - line.pop("role", None) - if not line.get("moving"): - line.pop("text", None) - line.pop("image", None) - line.pop("line_name", None) - context = { - "data_trade_date": setup["trade_date"], - "selected_focus": { - "sector": chart.get("sector") or "", - "stock": chart.get("stock") or {}, - }, - "hexagram": hexagram_context, - "movement": chart.get("movement") or {}, - } - else: - personal_profile = self.account_personal_field( - setup["calendar_date"], - setup["field"], - public=False, - ) - fortune_field = json.loads(json.dumps(setup["field"], ensure_ascii=False)) - catalog = fortune_field.pop("sector_catalog", []) - dominant_elements = { - item.get("element") for item in fortune_field.get("balance", [])[:2] - } - fortune_field["industry_affinity"] = [ - { - "element": group.get("element"), - "examples": [ - item.get("name") - for item in group.get("industries", [])[:8] - if item.get("name") - ], - } - for group in catalog - if group.get("element") in dominant_elements - ] - context = { - "calendar_date": setup["calendar_date"], - "five_phase_field": fortune_field, - "personal_profile": personal_profile, - } - context_date = setup["calendar_date"] - if mode == "trend": - context_date = setup["trade_date"] - else: - context = { - "hexagram": self.heaven_hexagram(payload.get("lines")), - "ritual": "用户已完成30秒静心、六次三枚铜钱起卦,并在心中察看第一念。问题未输入。", - } - context_date = trade_date - result, compiler = self._call_heaven_agent(mode, context) - subject, subject_detail = self._heaven_reading_identity( - mode, context_date, context - ) - dedupe_key = ( - f"fortune:{context_date}" - if mode == "fortune" - else f"{mode}:{context_date}:{secrets.token_urlsafe(12)}" - ) - reading = self.database.save_heaven_reading( - self.current_user_id, - mode, - context_date, - subject, - subject_detail, - str(result.get("answer") or ""), - context, - dedupe_key, - ) - return { - **result, - "mode": mode, - "compiler": compiler, - "notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "", - "reading": reading, - "reused": False, - } - - @staticmethod - def _legacy_truncated_heaven_reading(reading: dict[str, Any] | None) -> bool: - return bool(reading and str(reading.get("answer") or "").rstrip().endswith("……")) - - def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]: - result = self.llm_gateway.call( - f"heaven_{mode}", - f"heaven-{mode}-v1", - lambda profile: interpret_heaven( - mode, - context, - profile.api_key, - profile.base_url, - profile.model, - ), - (HeavenAgentError,), - ) - return result.value, result.role - - def _heaven_index_context( - self, - trade_date: str, - dashboard: dict[str, Any], - market_mode: str = "historical", - ) -> dict[str, Any]: - cached = self.database.get_data_snapshot("heaven_indices", trade_date) - cached_valid = False - if cached: - cached_rows = list(cached.get("indices") or []) - cached_dates = { - str(row.get("trade_date") or "").replace("-", "") - for row in cached_rows - } - cached_valid = ( - len(cached_rows) == 3 - and cached_dates == {trade_date} - and bool(cached.get("precise")) - and not cached.get("realtime") - and str(cached.get("source") or "") == "tushare" - and int(cached.get("schema_version") or 0) >= 3 - ) - if market_mode != "intraday" and cached_valid: - return cached - - if not self.configured: - error = "Tushare Token 未配置" - else: - try: - client = self._tushare_client() - if market_mode == "intraday": - payload = self._aggregate_index_context(trade_date) - payload["schema_version"] = 3 - return payload - payload = client.market_indices(trade_date) - payload["schema_version"] = 3 - if market_mode == "closed": - payload["finalized"] = True - self.database.save_data_snapshot( - "heaven_indices", - trade_date, - str(payload.get("source") or "tushare"), - payload, - ) - return payload - except Exception as exc: - error = str(exc) - overview = dashboard.get("overview") or {} - up_count = float(overview.get("up_count") or 0) - down_count = float(overview.get("down_count") or 0) - breadth = (up_count - down_count) / max(up_count + down_count, 1) - return { - "source": "market_breadth_proxy", - "trade_date": trade_date, - "realtime": False, - "precise": False, - "schema_version": 3, - "notice": f"指数数据不可用,当前以市场宽度代理:{error}", - "indices": [], - "aggregate": { - "average_pct_chg": round(breadth * 2.5, 3), - "average_return_5d": 0, - "average_return_20d": 0, - }, - } - - def _aggregate_index_context( - self, - trade_date: str, - tushare_error: str = "", - ) -> dict[str, Any]: - quotes = self.realtime_aggregator.tencent_indices() - epochs = [int(item.get("quote_time_epoch") or 0) for item in quotes] - quote_dates = { - datetime.fromtimestamp(epoch).astimezone().strftime("%Y%m%d") - for epoch in epochs if epoch - } - if len(quotes) != 3 or quote_dates != {trade_date}: - raise ValueError("腾讯三大指数日期与目标交易日不一致") - now = datetime.now().astimezone() - max_skew = 120 if now.hour >= 15 else 15 - if max(epochs) - min(epochs) > max_skew: - raise ValueError(f"腾讯三大指数时间差超过{max_skew}秒") - - code_map = { - "000001": "000001.SH", - "399001": "399001.SZ", - "399006": "399006.SZ", - } - client = self._tushare_client() - indices = [] - start_date = ( - datetime.strptime(trade_date, "%Y%m%d") - timedelta(days=20) - ).strftime("%Y%m%d") - for quote in quotes: - ts_code = code_map[str(quote.get("code") or "")] - history = client.query( - "index_daily", - {"ts_code": ts_code, "start_date": start_date, "end_date": trade_date}, - "ts_code,trade_date,close,pct_chg", - ) - history.sort(key=lambda item: str(item.get("trade_date") or "")) - completed_closes = [ - float(item.get("close") or 0) - for item in history - if str(item.get("trade_date") or "") < trade_date - and float(item.get("close") or 0) > 0 - ] - close_5d = ( - completed_closes[-5] - if len(completed_closes) >= 5 - else completed_closes[0] if completed_closes else 0 - ) - close = float(quote.get("price") or 0) - indices.append( - { - "ts_code": ts_code, - "name": quote.get("name") or ts_code, - "trade_date": trade_date, - "close": close, - "pct_chg": round(float(quote.get("change") or 0), 3), - "return_5d": round((close / close_5d - 1) * 100, 3) if close_5d else 0, - "return_20d": 0, - "amount_billion": float(quote.get("amount_billion") or 0), - "quote_time": quote.get("quote_time") or "", - } - ) - return { - "trade_date": trade_date, - "source": "+".join( - sorted({str(item.get("source") or "web_quote") for item in quotes}) - + ["tushare_index_daily"] - ), - "realtime": True, - "precise": True, - "indices": indices, - "aggregate": { - "average_pct_chg": round( - sum(item["pct_chg"] for item in indices) / len(indices), 3 - ), - "average_return_5d": round( - sum(item["return_5d"] for item in indices) / len(indices), 3 - ), - "average_return_20d": 0, - }, - "quote_time_skew_seconds": max(epochs) - min(epochs), - "notice": ( - "指数实时行情来自腾讯行情,5日趋势来自Tushare历史指数。" - + (f" Tushare实时指数未使用:{tushare_error}" if tushare_error else "") - ), - } - - def _heaven_sector_context( - self, - identifier: str, - trade_date: str, - market_mode: str = "historical", - ) -> dict[str, Any] | None: - """Return the Shenwan L2 sector context for heaven trend. - - 观势行业层只使用申万二级行业。外显盘中使用 rt_sw_k、历史使用 - sw_daily;内核独立使用目标日期成分股行情聚合。收盘过渡期在 - sw_daily 入库前接受同日15:00后的 rt_sw_k 收盘快照。 - """ - cache_key = f"{trade_date}:{identifier.strip().lower()}" - cached = self.database.get_data_snapshot("heaven_sector", cache_key) - cached_date = str((cached or {}).get("trade_date") or "").replace("-", "") - cached_valid = bool( - cached - and cached_date == trade_date - and cached.get("taxonomy") == "sw_l2" - and cached.get("inner_precise", cached.get("precise")) - and cached.get("outer_precise", cached.get("precise")) - and not cached.get("realtime") - and int(cached.get("schema_version") or 0) >= 6 - ) - if market_mode != "intraday" and cached_valid: - return cached - if not self.configured: - return None - try: - payload = self._tushare_client().sw_sector_snapshot( - tushare_code(identifier), - trade_date, - realtime_expected=market_mode == "intraday", - allow_realtime_close=market_mode == "closed", - ) - except TushareError as exc: - if cached_valid: - return cached - return { - "name": "", - "code": "", - "taxonomy": "sw_l2", - "source": "tushare", - "trade_date": trade_date, - "realtime": market_mode == "intraday", - "precise": False, - "inner_precise": False, - "outer_precise": False, - "coverage": 0, - "member_count": 0, - "quote_count": 0, - "error": f"申万二级行业数据获取失败:{exc}", - } - if not payload.get("realtime") and payload.get("precise"): - self.database.save_data_snapshot( - "heaven_sector", - cache_key, - str(payload.get("source") or "tushare"), - payload, - ) - return payload - SERVICE = DashboardService() @@ -1998,6 +713,7 @@ class RequestHandler( AccountHttpMixin, SystemHttpMixin, MentorHttpMixin, + HeavenHttpMixin, LLMHttpMixin, HttpTransportMixin, BaseHTTPRequestHandler, @@ -2733,28 +1449,3 @@ class RequestHandler( self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) except Exception as exc: self.send_json({"error": f"跟踪刷新失败:{exc}"}, HTTPStatus.INTERNAL_SERVER_ERROR) - - - def heaven_hexagram(self) -> None: - try: - body = self.read_json_body() - result = SERVICE.heaven_hexagram(body.get("lines")) - self.send_json({"ok": True, "hexagram": result}) - except (ValueError, json.JSONDecodeError) as exc: - self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) - - def heaven_personal(self) -> None: - try: - body = self.read_json_body() - result = SERVICE.heaven_personal(body) - self.send_json({"ok": True, "personal": result}) - except (ValueError, json.JSONDecodeError) as exc: - self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) - - def heaven_interpret(self) -> None: - try: - body = self.read_json_body() - result = SERVICE.heaven_interpret(body) - self.send_json({"ok": True, **result}) - except (ValueError, json.JSONDecodeError) as exc: - self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) diff --git a/app/backend/features/heaven/__init__.py b/app/backend/features/heaven/__init__.py new file mode 100644 index 0000000..81b2073 --- /dev/null +++ b/app/backend/features/heaven/__init__.py @@ -0,0 +1,24 @@ +from .agent import HeavenAgentError, interpret_heaven +from .engine import ( + build_five_phase_field, + build_market_hexagram, + build_manual_market_hexagram, + build_personal_field, + hexagram_from_lines, +) +from .http import HeavenHttpMixin +from .repository import HeavenRepositoryMixin +from .service import HeavenServiceMixin + +__all__ = [ + "HeavenAgentError", + "HeavenHttpMixin", + "HeavenRepositoryMixin", + "HeavenServiceMixin", + "build_five_phase_field", + "build_manual_market_hexagram", + "build_market_hexagram", + "build_personal_field", + "hexagram_from_lines", + "interpret_heaven", +] diff --git a/app/backend/features/heaven/agent.py b/app/backend/features/heaven/agent.py new file mode 100644 index 0000000..e4d5fcc --- /dev/null +++ b/app/backend/features/heaven/agent.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import json +import time +import urllib.error +import urllib.request +from typing import Any + + +class HeavenAgentError(RuntimeError): + pass + + +def interpret_heaven( + mode: str, + context: dict[str, Any], + api_key: str, + base_url: str, + model: str, + timeout: int = 90, +) -> dict[str, Any]: + if mode not in {"trend", "fortune", "heart"}: + raise HeavenAgentError("不支持的问天解读模式。") + if not api_key or not model: + raise HeavenAgentError("LLM API Key 或模型尚未配置。") + system_prompt = _system_prompt(mode) + payload = json.dumps( + { + "model": model, + "messages": [ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": json.dumps(context, ensure_ascii=False, separators=(",", ":")), + }, + ], + "stream": False, + }, + ensure_ascii=False, + ).encode("utf-8") + request = urllib.request.Request( + f"{base_url.rstrip('/')}/chat/completions", + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + "User-Agent": "XiaobaiReviewWeb/0.7", + }, + method="POST", + ) + started = time.perf_counter() + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + result = json.loads(response.read().decode("utf-8")) + answer = str(result["choices"][0]["message"]["content"]).strip() + if not answer: + raise KeyError("empty response") + except urllib.error.HTTPError as exc: + raise HeavenAgentError(_http_error_message(exc)) from exc + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as exc: + raise HeavenAgentError(f"问天模型调用失败:{exc}") from exc + return { + "answer": answer, + "model": model, + "latency_ms": round((time.perf_counter() - started) * 1000), + } + + +def _system_prompt(mode: str) -> str: + common = """ +你是“小白复盘”的问天解读器。所有历法、卦象、爻位和市场指标已经由确定性程序计算,你只能解释提供的数据,不得改卦、改爻、改干支或编造行情。 +问天属于传统文化与娱乐化观察,不是预测模型,不承诺应验,不输出无条件买卖指令,不用神秘话术制造确定性。 +使用中文,先给核心判断,再解释结构。引用市场数字时标明数据日期。输出纯文本,可使用简短标题。 +""".strip() + if mode == "trend": + return common + """ + +当前任务是“观势·解势”。六爻从初爻到上爻依次是个股内核、个股外显、板块内核、板块外显、指数内核、指数外显;初二为地、三四为人、五上为天。 +行情数据只负责生成六爻,本次解势必须以卦象本身为主,不得根据指数涨跌、板块强弱、涨停家数、成交量或个股表现直接推演方向。context中不会提供这些数字,也不会提供爻位对应的市场角色。 +先解释本卦卦名的核心义、上下卦组合及大象;再只解释实际动爻所代表的转折,并说明本卦如何走向之卦;最后可把这一组卦势翻译成克制的市场语言。 +重点是“本卦为当下之势,动爻为变化关节,之卦为所趋之势”。不要说明某一动爻对应指数、板块或个股,也不要输出“一看指数、二看涨停家数”一类行情观察条件。 +全文控制在300至450个中文字符,最多四小段。卦理约占九成,市场翻译最多一句,只能落到节制、等待、守信、辨伪等行为态度,不得据此预测市场下一阶段、涨跌方向或动能变化。不直接荐股,不使用Markdown表格。 +不要使用“必然、确定、必涨、必跌、后续将、进入某阶段”等断语;天机只点出势的性质与变化关系,不替用户宣布结果。 +""".strip() + if mode == "fortune": + return common + """ + +当前任务是“观气·解运”。严格区分五运、六气、节气、月令和日干,不把丙午简单解释为火年。 +严格服从five_phase_field.framework提供的确定性结构,不自行重新计算五行:年纲由中运与司天在泉构成;岁半以前司天为主、在泉为辅,岁半以后在泉为主、司天为辅;当前六气层以客气加临主气为核心;日辰只负责触发。节气只用于定位当前六气阶段,不得再次叠加为独立力量。 +重点解释framework.relations中的客主同气、客生主、主生客、客克主或主克客,以及客胜为从、主胜为逆、司天在泉同位、天符岁会等已经判定的关系。不得把司天、在泉、主气、客气视为彼此独立的证据重复计权,也不得自行增删传统格局。 +首要解释当日气场容易放大参与者的哪些情绪、判断偏差和操作冲动,例如急躁、恐惧、迟疑、追涨、过早止损或路径依赖;再给出一至两个调节动作。 +如有personal_profile,结合其日主、十神、五行平衡倾向说明当日对该用户主观状态的影响,但不得把简化平衡倾向说成唯一喜用神,也不得复述或猜测出生日期。 +不得引用市场上涨下跌家数、涨跌停数量、成交额、板块强度或个股表现来证明气场。industry_affinity只是五行行业取象示例,不是行情旁证;行业契合度最多在末尾用一句话说明,不得写“当日共振”或暗示相关行业必然涨跌。 +全文控制在420至600个中文字符,按“三层气机、人的状态、操作偏向、个人影响(如有)、制衡动作”组织,标题必须写“三层气机”。明确这些是传统历法框架下的观察语言,不宣称气候或五行直接导致股价。 +""".strip() + return common + """ + +当前任务是“观心·解卦”。用户的问题始终只在心中,没有输入给你,因此你不能猜测问题内容,也不能替用户作具体决定。 +全文控制在180至350个中文字符。只写一句卦意;一小段动爻与之卦;最后三句极短的问心句。 +不要重述六条爻辞,不猜用户未说出口的问题,不以吉凶二字替代思考,不给出股票涨跌预测。语气安静、克制,越短越有余味。 +""".strip() + + +def _http_error_message(exc: urllib.error.HTTPError) -> str: + detail = "" + try: + payload = json.loads(exc.read().decode("utf-8", errors="replace")) + error = payload.get("error") + if isinstance(error, dict): + detail = str(error.get("message") or error.get("code") or "") + elif error: + detail = str(error) + elif payload.get("message"): + detail = str(payload["message"]) + except (json.JSONDecodeError, OSError): + detail = "" + suffix = f":{detail[:300]}" if detail else "" + return f"问天模型调用失败(HTTP {exc.code}){suffix}" diff --git a/app/backend/features/heaven/engine.py b/app/backend/features/heaven/engine.py new file mode 100644 index 0000000..b6cd0bf --- /dev/null +++ b/app/backend/features/heaven/engine.py @@ -0,0 +1,1181 @@ +from __future__ import annotations + +import json +import math +import sys +from datetime import datetime +from functools import lru_cache +from typing import Any + +from backend.bootstrap.config import APP_DIR + +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 diff --git a/app/backend/features/heaven/http.py b/app/backend/features/heaven/http.py new file mode 100644 index 0000000..0defe12 --- /dev/null +++ b/app/backend/features/heaven/http.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import json +from http import HTTPStatus + + +class HeavenHttpMixin: + def heaven_hexagram(self) -> None: + try: + body = self.read_json_body() + result = self.application_service.heaven_hexagram(body.get("lines")) + self.send_json({"ok": True, "hexagram": result}) + except (ValueError, json.JSONDecodeError) as exc: + self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) + + def heaven_personal(self) -> None: + try: + body = self.read_json_body() + result = self.application_service.heaven_personal(body) + self.send_json({"ok": True, "personal": result}) + except (ValueError, json.JSONDecodeError) as exc: + self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) + + def heaven_interpret(self) -> None: + try: + body = self.read_json_body() + result = self.application_service.heaven_interpret(body) + self.send_json({"ok": True, **result}) + except (ValueError, json.JSONDecodeError) as exc: + self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST) diff --git a/app/backend/features/heaven/repository.py b/app/backend/features/heaven/repository.py new file mode 100644 index 0000000..0dc8115 --- /dev/null +++ b/app/backend/features/heaven/repository.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import json +import sqlite3 +from datetime import datetime +from typing import Any + + +class HeavenRepositoryMixin: + @staticmethod + def _heaven_reading_dict(row: sqlite3.Row | None) -> dict[str, Any] | None: + if not row: + return None + return { + "id": int(row["id"]), + "mode": str(row["mode"]), + "context_date": str(row["context_date"]), + "subject": str(row["subject"]), + "subject_detail": str(row["subject_detail"]), + "answer": str(row["answer"]), + "created_at": str(row["created_at"]), + } + + def save_heaven_reading( + self, + user_id: int, + mode: str, + context_date: str, + subject: str, + subject_detail: str, + answer: str, + context_snapshot: dict[str, Any], + dedupe_key: str, + ) -> dict[str, Any]: + now = datetime.now().astimezone().isoformat(timespec="seconds") + snapshot_json = json.dumps( + context_snapshot, ensure_ascii=False, separators=(",", ":") + ) + with self.connect() as connection: + connection.execute( + """ + INSERT INTO heaven_readings + (user_id, mode, context_date, subject, subject_detail, answer, + context_snapshot, dedupe_key, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id, dedupe_key) DO NOTHING + """, + ( + int(user_id), mode, context_date, subject, subject_detail, + answer, snapshot_json, dedupe_key, now, + ), + ) + row = connection.execute( + """ + SELECT id, mode, context_date, subject, subject_detail, answer, created_at + FROM heaven_readings WHERE user_id = ? AND dedupe_key = ? + """, + (int(user_id), dedupe_key), + ).fetchone() + connection.execute( + """ + DELETE FROM heaven_readings + WHERE user_id = ? AND mode = ? AND id NOT IN ( + SELECT id FROM heaven_readings + WHERE user_id = ? AND mode = ? ORDER BY id DESC LIMIT 100 + ) + """, + (int(user_id), mode, int(user_id), mode), + ) + result = self._heaven_reading_dict(row) + if not result: + raise ValueError("解读记录保存失败。") + return result + + def list_heaven_readings( + self, + user_id: int, + mode: str, + context_date: str = "", + limit: int = 100, + ) -> list[dict[str, Any]]: + clauses = ["user_id = ?", "mode = ?"] + parameters: list[Any] = [int(user_id), mode] + if context_date: + clauses.append("context_date = ?") + parameters.append(context_date) + parameters.append(max(1, min(100, int(limit)))) + with self.connect() as connection: + rows = connection.execute( + f""" + SELECT id, mode, context_date, subject, subject_detail, answer, created_at + FROM heaven_readings WHERE {' AND '.join(clauses)} + ORDER BY context_date DESC, id DESC LIMIT ? + """, + parameters, + ).fetchall() + return [self._heaven_reading_dict(row) for row in rows if row] + + def latest_heaven_reading( + self, user_id: int, mode: str, context_date: str = "" + ) -> dict[str, Any] | None: + items = self.list_heaven_readings(user_id, mode, context_date, 1) + return items[0] if items else None + + def delete_heaven_reading(self, user_id: int, reading_id: int) -> bool: + with self.connect() as connection: + cursor = connection.execute( + "DELETE FROM heaven_readings WHERE id = ? AND user_id = ?", + (int(reading_id), int(user_id)), + ) + return cursor.rowcount > 0 diff --git a/app/backend/features/heaven/service.py b/app/backend/features/heaven/service.py new file mode 100644 index 0000000..86f5936 --- /dev/null +++ b/app/backend/features/heaven/service.py @@ -0,0 +1,1303 @@ +from __future__ import annotations + +import copy +import json +import re +from datetime import date, datetime, timedelta +from typing import Any + +from backend.bootstrap.config import ( + normalize_date, + tushare_code, + validate_stock_code, + validate_text, +) +from backend.data.providers.tushare_client import TushareError, _sector_coverage_issue +from backend.features.heaven.agent import HeavenAgentError, interpret_heaven +from backend.features.heaven.engine import ( + _market_line_scores, + _score_to_line, + build_five_phase_field, + build_market_hexagram, + hexagram_from_lines, +) +from backend.features.market import MarketServiceMixin + + +class HeavenServiceMixin: + @staticmethod + def _heaven_manual_schema(market_mode: str) -> dict[str, dict[str, Any]]: + intraday = market_mode == "intraday" + fields = { + "stock_amount_percentile": {"line": 1, "label": "成交额全市场分位", "unit": "%", "min": 0, "max": 100}, + "stock_turnover_rate": {"line": 1, "label": "个股换手率", "unit": "%", "min": 0, "max": 100}, + "stock_turnover_relative": {"line": 1, "label": "相对市场换手", "unit": "倍", "min": 0, "max": 20}, + "stock_volume_activity_ratio": {"line": 1, "label": "同进度量能", "unit": "倍", "min": 0, "max": 20}, + "stock_seal_amount_million": {"line": 1, "label": "封单金额", "unit": "万元", "min": 0, "max": 100000000}, + "stock_open_times": {"line": 1, "label": "开板次数", "unit": "次", "min": 0, "max": 100, "integer": True}, + "stock_change": {"line": 2, "label": "个股涨跌幅", "unit": "%", "min": -100, "max": 100}, + "stock_streak": {"line": 2, "label": "连板高度", "unit": "板", "min": 0, "max": 100, "integer": True}, + "stock_status": {"line": 2, "label": "个股状态", "type": "select", "options": ["普通", "涨停", "炸板", "跌停"]}, + "sector_name": {"line": [3, 4], "label": "申万二级行业", "type": "text", "max_length": 50}, + "sector_up_count": {"line": 3, "label": "行业上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "sector_down_count": {"line": 3, "label": "行业下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "sector_coverage": {"line": 3, "label": "成分行情覆盖率", "unit": "%", "min": 0, "max": 100}, + "sector_relative_turnover": {"line": 3, "label": "行业相对市场换手", "unit": "倍", "min": 0, "max": 20}, + "sector_member_equal_change": {"line": 3, "label": "成分等权涨跌幅", "unit": "%", "min": -100, "max": 100}, + "sector_change": {"line": 4, "label": "申万官方涨跌幅", "unit": "%", "min": -100, "max": 100}, + "sector_leading_pct": {"line": [3, 4], "label": "行业领涨股涨跌幅", "unit": "%", "min": -100, "max": 100}, + "market_sentiment_score": {"line": 5, "label": "市场情绪温度", "unit": "分", "min": 0, "max": 100}, + "market_seal_rate": {"line": 5, "label": "封板率", "unit": "%", "min": 0, "max": 100}, + "market_amount_billion": {"line": 5, "label": "两市成交额", "unit": "亿元", "min": 0, "max": 10000000}, + "market_recent_average_amount_billion": {"line": 5, "label": "近期平均成交额", "unit": "亿元", "min": 0, "max": 10000000}, + "market_up_count": {"line": 5, "label": "上涨家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "market_down_count": {"line": 5, "label": "下跌家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "market_limit_up_count": {"line": 5, "label": "涨停家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "market_limit_down_count": {"line": 5, "label": "跌停家数", "unit": "家", "min": 0, "max": 10000, "integer": True}, + "index_sh_change": {"line": 6, "label": "上证指数涨跌幅", "unit": "%", "min": -20, "max": 20}, + "index_sz_change": {"line": 6, "label": "深证成指涨跌幅", "unit": "%", "min": -20, "max": 20}, + "index_cy_change": {"line": 6, "label": "创业板指涨跌幅", "unit": "%", "min": -20, "max": 20}, + "note": {"line": [], "label": "补录说明", "type": "text", "max_length": 200}, + } + if intraday: + for key in ("stock_seal_amount_million", "stock_open_times"): + fields.pop(key) + else: + for key in ("stock_turnover_relative", "stock_volume_activity_ratio", "sector_relative_turnover"): + fields.pop(key) + return fields + + @classmethod + def _validate_heaven_manual_data( + cls, raw: Any, market_mode: str + ) -> dict[str, Any]: + if raw in (None, ""): + return {} + if not isinstance(raw, dict): + raise ValueError("六爻补录数据格式不正确。") + schema = cls._heaven_manual_schema(market_mode) + unknown = set(raw) - set(schema) + if unknown: + raise ValueError(f"六爻补录包含未知字段:{next(iter(sorted(unknown)))}") + values: dict[str, Any] = {} + for key, value in raw.items(): + if value is None or (isinstance(value, str) and not value.strip()): + continue + spec = schema[key] + if spec.get("type") == "text": + values[key] = validate_text(value, spec["label"], int(spec["max_length"])) + continue + if spec.get("type") == "select": + text = str(value).strip() + if text not in spec["options"]: + raise ValueError(f"{spec['label']}不在允许范围内。") + values[key] = text + continue + try: + number = float(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{spec['label']}必须是数字。") from exc + if number < float(spec["min"]) or number > float(spec["max"]): + raise ValueError( + f"{spec['label']}应在 {spec['min']} 至 {spec['max']} 之间。" + ) + values[key] = int(number) if spec.get("integer") else number + return values + + @staticmethod + def _apply_heaven_manual_data( + dashboard: dict[str, Any], + index_context: dict[str, Any], + sector: dict[str, Any] | None, + stock: dict[str, Any] | None, + manual_data: dict[str, Any], + market_mode: str, + trade_date: str, + stock_code: str, + ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]: + dashboard = copy.deepcopy(dashboard) + index_context = copy.deepcopy(index_context or {}) + sector = copy.deepcopy(sector or {}) + stock = copy.deepcopy(stock or {}) + overview = dashboard.setdefault("overview", {}) + + stock_map = { + "stock_amount_percentile": "amount_percentile", + "stock_turnover_rate": "turnover_rate", + "stock_turnover_relative": "turnover_relative", + "stock_volume_activity_ratio": "volume_activity_ratio", + "stock_seal_amount_million": "seal_amount_million", + "stock_open_times": "open_times", + "stock_change": "change", + "stock_streak": "streak", + "stock_status": "status", + } + sector_map = { + "sector_name": "name", + "sector_up_count": "up_count", + "sector_down_count": "down_count", + "sector_coverage": "coverage", + "sector_relative_turnover": "relative_turnover", + "sector_member_equal_change": "member_equal_change", + "sector_change": "change", + "sector_leading_pct": "leading_pct", + } + overview_map = { + "market_sentiment_score": "sentiment_score", + "market_seal_rate": "seal_rate", + "market_amount_billion": "amount_billion", + "market_recent_average_amount_billion": "recent_average_amount_billion", + "market_up_count": "up_count", + "market_down_count": "down_count", + "market_limit_up_count": "limit_up_count", + "market_limit_down_count": "limit_down_count", + } + for manual_key, target in stock_map.items(): + if manual_key in manual_data: + stock[target] = manual_data[manual_key] + for manual_key, target in sector_map.items(): + if manual_key in manual_data: + sector[target] = manual_data[manual_key] + for manual_key, target in overview_map.items(): + if manual_key in manual_data: + overview[target] = manual_data[manual_key] + + if any(key.startswith("stock_") for key in manual_data): + stock.setdefault("code", stock_code) + stock.setdefault("name", stock_code or "--") + stock["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical" + if market_mode == "intraday" and "stock_volume_activity_ratio" in manual_data: + stock["activity_source"] = "user_supplied" + if any(key.startswith("sector_") for key in manual_data): + sector["_quantitative_mode"] = "intraday" if market_mode == "intraday" else "historical" + sector.setdefault("taxonomy", "sw_l2") + + index_keys = ( + ("index_sh_change", "000001.SH", "上证指数"), + ("index_sz_change", "399001.SZ", "深证成指"), + ("index_cy_change", "399006.SZ", "创业板指"), + ) + rows = {str(row.get("ts_code") or row.get("code") or ""): dict(row) for row in index_context.get("indices") or []} + for manual_key, code, name in index_keys: + if manual_key not in manual_data: + continue + row = rows.get(code, {"ts_code": code, "name": name}) + row.update({"pct_chg": manual_data[manual_key], "trade_date": trade_date}) + rows[code] = row + ordered_rows = [rows.get(code) for _, code, _ in index_keys] + if all(ordered_rows): + index_context["indices"] = ordered_rows + changes = [float(row.get("pct_chg") or 0) for row in ordered_rows] + aggregate = dict(index_context.get("aggregate") or {}) + aggregate["average_pct_chg"] = sum(changes) / 3 + index_context["aggregate"] = aggregate + return dashboard, index_context, sector, stock + + @classmethod + def _heaven_line_checks( + cls, + trade_date: str, + dashboard: dict[str, Any], + recent_history: list[dict[str, Any]], + index_context: dict[str, Any], + sector: dict[str, Any], + stock: dict[str, Any], + market_mode: str, + manual_data: dict[str, Any], + ) -> list[dict[str, Any]]: + intraday = market_mode == "intraday" + closed = market_mode == "closed" + schema = cls._heaven_manual_schema(market_mode) + required = { + 1: (["stock_amount_percentile", "stock_turnover_relative", "stock_volume_activity_ratio"] if intraday else ["stock_amount_percentile", "stock_turnover_rate", "stock_seal_amount_million", "stock_open_times"]), + 2: ["stock_change", "stock_streak", "stock_status"], + 3: (["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_relative_turnover"] if intraday else ["sector_name", "sector_up_count", "sector_down_count", "sector_coverage", "sector_member_equal_change", "sector_leading_pct"]), + 4: ["sector_name", "sector_change", "sector_leading_pct"], + 5: ["market_sentiment_score", "market_seal_rate", "market_amount_billion", "market_recent_average_amount_billion", "market_up_count", "market_down_count", "market_limit_up_count", "market_limit_down_count"], + 6: ["index_sh_change", "index_sz_change", "index_cy_change"], + } + names = { + 1: ("初爻", "个股内核", "成交活跃、换手与量能"), + 2: ("二爻", "个股外显", "涨跌、连板与状态"), + 3: ("三爻", "行业内核", "行业宽度与成交活跃"), + 4: ("四爻", "行业外显", "行业涨跌与领涨表现"), + 5: ("五爻", "市场内核", "情绪、封板、成交与市场宽度"), + 6: ("上爻", "指数外显", "三大指数当日涨跌"), + } + + index_date = str(index_context.get("trade_date") or "").replace("-", "") + index_rows = list(index_context.get("indices") or []) + index_dates = {str(row.get("trade_date") or "").replace("-", "") for row in index_rows} + index_issues = [] + if len(index_rows) < 3: + index_issues.append(f"三大指数仅取得 {len(index_rows)}/3 条行情") + elif index_date != trade_date or index_dates != {trade_date}: + actual_dates = "、".join(sorted(value for value in index_dates if value)) or "未知" + index_issues.append(f"指数实际日期为 {actual_dates},目标交易日为 {trade_date}") + elif not index_context.get("precise"): + index_issues.append("三大指数行情未通过完整性校验") + elif intraday and not index_context.get("realtime"): + index_issues.append("盘中缺少可核验的实时指数行情") + elif not intraday and (index_context.get("realtime") or str(index_context.get("source") or "") != "tushare"): + index_issues.append("收盘或历史行情不是官方指数日线") + + sector_date = str(sector.get("trade_date") or "").replace("-", "") + sector_coverage = float(sector.get("coverage") or 0) + sector_explained_count = int( + sector.get("explained_count") + if sector.get("explained_count") is not None + else sector.get("quote_count") or 0 + ) + sector_explained_coverage = float( + sector.get("explained_coverage") + if sector.get("explained_coverage") is not None + else sector_coverage + ) + sector_coverage_issue = _sector_coverage_issue( + int(sector.get("member_count") or 0), + int(sector.get("quote_count") or 0), + sector_explained_coverage, + sector_explained_count, + ) + sector_common = [] + if not sector: + sector_common.append("未取得申万二级行业归属") + elif sector.get("taxonomy") != "sw_l2": + sector_common.append("行业分类不是申万二级") + elif sector_date != trade_date: + sector_common.append("行业行情日期与目标交易日不一致") + elif intraday and not sector.get("realtime"): + sector_common.append("盘中行业行情不是申万实时行情") + elif market_mode == "historical" and sector.get("realtime"): + sector_common.append("历史行业行情不能使用实时快照") + elif closed and sector.get("realtime") and not sector.get("finalized"): + sector_common.append("收盘行业实时行情尚未形成15:00最终快照") + sector_inner = list(sector_common) + sector_outer = list(sector_common) + if not sector.get("inner_precise", sector.get("precise")): + sector_inner.append(str(sector.get("inner_error") or sector.get("error") or "行业内核数据未通过校验")) + if not sector.get("outer_precise", sector.get("precise")): + sector_outer.append(str(sector.get("outer_error") or sector.get("error") or "行业外显数据未通过校验")) + if sector and sector_coverage_issue and sector_coverage_issue not in sector_inner: + sector_inner.append(sector_coverage_issue) + if sector.get("realtime") and not sector.get("relative_turnover"): + sector_inner.append("缺少行业相对全市场换手活跃度") + + stock_date = str(stock.get("trade_date") or "").replace("-", "") + stock_common = [] + if not stock.get("code"): + stock_common.append("尚未载入有效个股") + elif stock_date != trade_date: + stock_common.append(f"个股实际日期为 {stock_date or '未知'},目标交易日为 {trade_date}") + elif not stock.get("precise"): + stock_common.append("个股行情未通过完整性校验") + elif intraday and not stock.get("realtime"): + stock_common.append("盘中个股行情不是实时行情") + elif not intraday and (stock.get("realtime") or str(stock.get("data_source") or "") != "tushare"): + stock_common.append("收盘或历史个股行情不是官方日线") + stock_inner = list(stock_common) + if intraday and stock.get("turnover_source") in {None, "", "unavailable"}: + stock_inner.append("缺少可核验的实时换手率") + if intraday and stock.get("activity_source") in {None, "", "unavailable"}: + stock_inner.append("缺少同时间进度量能基准") + + overview = dashboard.get("overview") or {} + market_key_map = { + "market_sentiment_score": "sentiment_score", "market_seal_rate": "seal_rate", + "market_amount_billion": "amount_billion", "market_recent_average_amount_billion": "recent_average_amount_billion", + "market_up_count": "up_count", "market_down_count": "down_count", + "market_limit_up_count": "limit_up_count", "market_limit_down_count": "limit_down_count", + } + market_issues = [] + for manual_key, source_key in market_key_map.items(): + if source_key == "recent_average_amount_billion": + history_values = [item.get("amount_billion") for item in recent_history[:-1] if item.get("amount_billion") is not None] + if source_key not in overview and not history_values: + market_issues.append(f"缺少{schema[manual_key]['label']}") + elif source_key not in overview or overview.get(source_key) is None: + market_issues.append(f"缺少{schema[manual_key]['label']}") + + automatic_issues = { + 1: stock_inner, 2: stock_common, 3: sector_inner, + 4: sector_outer, 5: market_issues, 6: index_issues, + } + limits = list(dashboard.get("limits") or []) + scores = _market_line_scores(dashboard, recent_history, index_context, sector, stock, limits) + + value_map: dict[str, Any] = { + "stock_amount_percentile": stock.get("amount_percentile"), + "stock_turnover_rate": stock.get("turnover_rate"), + "stock_turnover_relative": stock.get("turnover_relative"), + "stock_volume_activity_ratio": stock.get("volume_activity_ratio"), + "stock_seal_amount_million": stock.get("seal_amount_million"), + "stock_open_times": stock.get("open_times"), + "stock_change": stock.get("change"), "stock_streak": stock.get("streak"), + "stock_status": stock.get("status"), "sector_name": sector.get("name"), + "sector_up_count": sector.get("up_count"), "sector_down_count": sector.get("down_count"), + "sector_coverage": sector.get("coverage"), "sector_relative_turnover": sector.get("relative_turnover"), + "sector_member_equal_change": sector.get("member_equal_change"), + "sector_change": sector.get("change"), "sector_leading_pct": sector.get("leading_pct"), + "market_sentiment_score": overview.get("sentiment_score"), "market_seal_rate": overview.get("seal_rate"), + "market_amount_billion": overview.get("amount_billion"), + "market_recent_average_amount_billion": overview.get("recent_average_amount_billion"), + "market_up_count": overview.get("up_count"), "market_down_count": overview.get("down_count"), + "market_limit_up_count": overview.get("limit_up_count"), "market_limit_down_count": overview.get("limit_down_count"), + } + history_values = [float(item.get("amount_billion")) for item in recent_history[:-1] if item.get("amount_billion") is not None] + if value_map["market_recent_average_amount_billion"] is None and history_values: + value_map["market_recent_average_amount_billion"] = sum(history_values) / len(history_values) + if value_map["stock_amount_percentile"] is None and not intraday: + amount = float(stock.get("amount_billion") or 0) + amounts = [float(item.get("amount_billion") or 0) for item in limits if item.get("amount_billion") is not None] + value_map["stock_amount_percentile"] = ( + sum(item <= amount for item in amounts) / len(amounts) * 100 if amounts else None + ) + row_by_code = {str(row.get("ts_code") or row.get("code") or ""): row for row in index_context.get("indices") or []} + value_map.update({ + "index_sh_change": (row_by_code.get("000001.SH") or {}).get("pct_chg"), + "index_sz_change": (row_by_code.get("399001.SZ") or {}).get("pct_chg"), + "index_cy_change": (row_by_code.get("399006.SZ") or {}).get("pct_chg"), + }) + + def missing_value(key: str) -> bool: + value = value_map.get(key) + return value is None or (isinstance(value, str) and not value.strip()) + + invalid_fields = { + line_number: {key for key in keys if missing_value(key)} + for line_number, keys in required.items() + } + if stock_common: + invalid_fields[1].update(required[1]) + invalid_fields[2].update(required[2]) + else: + if intraday and stock.get("turnover_source") in {None, "", "unavailable"}: + invalid_fields[1].add("stock_turnover_relative") + if intraday and stock.get("activity_source") in {None, "", "unavailable"}: + invalid_fields[1].add("stock_volume_activity_ratio") + + if sector_common: + invalid_fields[3].update(required[3]) + invalid_fields[4].update(required[4]) + else: + if not sector.get("inner_precise", sector.get("precise")) or sector_coverage_issue: + invalid_fields[3].update(key for key in required[3] if key != "sector_name") + if sector.get("realtime") and not sector.get("relative_turnover"): + invalid_fields[3].add("sector_relative_turnover") + # The official SW index supplies only the sector's external change. A valid + # membership name and member-stock leader remain usable when that quote fails. + if not sector.get("outer_precise", sector.get("precise")): + invalid_fields[4].add("sector_change") + + if index_issues: + invalid_fields[6].update(required[6]) + + checks = [] + for line_number in range(1, 7): + manual_keys = [key for key in required[line_number] if key in manual_data] + unresolved_fields = [ + key for key in required[line_number] + if key in invalid_fields[line_number] and key not in manual_data + ] + hard_missing_identity = line_number in {1, 2} and not stock.get("code") + passed = not hard_missing_identity and not unresolved_fields + status = "manual" if passed and manual_keys else "passed" if passed else "failed" + reasons = [] if passed else [ + *( ["请先输入并载入股票代码或名称"] if hard_missing_identity else automatic_issues[line_number] ), + *( ["需补充:" + "、".join(schema[key]["label"] for key in unresolved_fields)] if unresolved_fields else [] ), + ] + score = float(scores[line_number - 1]["score"]) + position, layer, formula = names[line_number] + checks.append({ + "line": line_number, "position": position, "layer": layer, "formula": formula, + "status": status, "passed": passed, "reasons": reasons, + "score": round(score, 3) if passed else None, + "line_value": _score_to_line(score) if passed else None, + "evidence": scores[line_number - 1]["evidence"] if passed else [], + "fields": [ + { + "key": key, "label": schema[key]["label"], "unit": schema[key].get("unit", ""), + "type": schema[key].get("type", "number"), "options": schema[key].get("options", []), + "value": value_map.get(key), "manual": key in manual_data, + "required": True, "min": schema[key].get("min"), "max": schema[key].get("max"), + "integer": bool(schema[key].get("integer")), + } + for key in required[line_number] + ], + }) + return checks + + def _resolve_heaven_stock_code(self, query: str) -> str: + raw = validate_text(query, "股票代码或名称", 30, required=True) + code_match = re.fullmatch(r"(\d{6})(?:\.(?:SH|SZ|BJ))?", raw.upper()) + if code_match: + return validate_stock_code(code_match.group(1)) + + candidates = self.database.search_stock_master(raw) + exact = [item for item in candidates if str(item.get("name") or "").casefold() == raw.casefold()] + if not exact and self.configured: + try: + rows = self._tushare_client().query( + "stock_basic", + {"name": raw, "list_status": "L"}, + "ts_code,symbol,name,industry,market,list_date", + ) + except TushareError: + rows = [] + if rows: + self.database.upsert_stock_master(rows) + candidates = self.database.search_stock_master(raw) + exact = [ + item + for item in candidates + if str(item.get("name") or "").casefold() == raw.casefold() + ] + + matches = exact or candidates + if len(matches) == 1: + return validate_stock_code(str(matches[0].get("code") or "")) + if len(matches) > 1: + choices = "、".join( + f"{item.get('name') or '--'}({item.get('code') or '--'})" + for item in matches[:5] + ) + raise ValueError(f"匹配到多只股票:{choices}。请输入六位股票代码。") + raise ValueError(f"未找到股票“{raw}”,请检查名称或输入六位股票代码。") + + def heaven_setup( + self, + trade_date: str, + sector_name: str = "", + stock_code: str = "", + manual_data: dict[str, Any] | None = None, + ) -> dict[str, Any]: + normalized_date = normalize_date(trade_date) + dashboard = self.get_dashboard(normalized_date) + data_date = normalize_date(str(dashboard.get("meta", {}).get("trade_date") or normalized_date)) + recent_history = self.database.snapshot_summaries(data_date, 10) + market_mode = self._heaven_market_mode(data_date, dashboard) + manual_data = self._validate_heaven_manual_data(manual_data, market_mode) + index_context = self._heaven_index_context(data_date, dashboard, market_mode) + external_stock = None + normalized_stock_code = "" + if stock_code.strip(): + normalized_stock_code = self._resolve_heaven_stock_code(stock_code) + external_stock = self._heaven_stock_context( + normalized_stock_code, + data_date, + dashboard, + market_mode, + ) + external_sector = None + if normalized_stock_code and self.configured: + external_sector = self._heaven_sector_context( + normalized_stock_code, + data_date, + market_mode, + ) + if external_sector and external_stock: + external_stock["sector"] = external_sector.get("name") or external_stock.get("sector") + dashboard, index_context, external_sector, external_stock = self._apply_heaven_manual_data( + dashboard, + index_context, + external_sector, + external_stock, + manual_data, + market_mode, + data_date, + normalized_stock_code, + ) + if external_sector and external_stock: + external_stock["sector"] = external_sector.get("name") or external_stock.get("sector") + sector_input = str((external_sector or {}).get("name") or sector_name.strip()) + if not normalized_stock_code: + data_checks = [] + chart = { + "available": False, + "selection_required": True, + "data_trade_date": data_date, + "sector": "", + "sector_code": "", + "sector_taxonomy": "", + "stock": {"code": "", "name": "", "status": ""}, + "quality": { + "status": "awaiting_selection", + "issues": [], + "principle": "", + "sources": [], + }, + "index_context": index_context, + } + else: + data_checks = self._heaven_line_checks( + data_date, + dashboard, + recent_history, + index_context, + external_sector or {}, + external_stock or {}, + market_mode, + manual_data, + ) + quality_issues = [ + f"{check['position']}·{check['layer']}:{';'.join(check['reasons'])}" + for check in data_checks + if not check["passed"] + ] + if quality_issues: + chart = { + "available": False, + "selection_required": False, + "data_trade_date": data_date, + "sector": str((external_sector or {}).get("name") or sector_input or "--"), + "sector_code": str((external_sector or {}).get("code") or ""), + "sector_taxonomy": str((external_sector or {}).get("taxonomy") or ""), + "stock": { + "code": normalized_stock_code, + "name": str((external_stock or {}).get("name") or "--"), + "status": str((external_stock or {}).get("status") or ""), + }, + "quality": { + "status": "blocked", + "issues": quality_issues, + "principle": "六爻任一层缺少同日、同口径的有效数据,本系统不成卦。", + "sources": self._heaven_trend_sources( + data_date, index_context, external_sector, external_stock + ), + }, + "index_context": index_context, + } + else: + chart = build_market_hexagram( + dashboard, + recent_history, + index_context, + sector_input, + normalized_stock_code, + external_stock, + external_sector, + ) + chart["available"] = True + chart["selection_required"] = False + manual_active = any(check["status"] == "manual" for check in data_checks) + chart["quality"] = { + "status": "manual" if manual_active else "verified", + "issues": [], + "principle": ( + "自动行情与用户补充数据均已通过同一套量化公式校验。" + if manual_active + else "指数、板块、个股均已通过同日同口径校验。" + ), + "sources": [ + *self._heaven_trend_sources( + data_date, index_context, external_sector, external_stock + ), + *([{ + "lines": "补录爻位", + "layer": "用户补充", + "realtime": market_mode == "intraday", + "detail": str(manual_data.get("note") or "量化数据经原公式重新计算"), + }] if manual_active else []), + ], + } + chart["data_checks"] = data_checks + chart["manual_data"] = manual_data + sector_phase_overrides = self.database.list_sector_phase_overrides() + field = build_five_phase_field( + normalized_date, + sector_phase_overrides, + ) + personal_profile = self.account_personal_field( + normalized_date, + field, + public=True, + ) + daily_fortune_reading = self.database.latest_heaven_reading( + self.current_user_id, "fortune", normalized_date + ) + if self._legacy_truncated_heaven_reading(daily_fortune_reading): + daily_fortune_reading = None + return { + "trade_date": data_date, + "calendar_date": normalized_date, + "market_mode": market_mode, + "chart": chart, + "field": field, + "personal_profile": personal_profile, + "daily_fortune_reading": daily_fortune_reading, + "sector_phase_overrides": [ + {"name": name, "element": element} + for name, element in sector_phase_overrides.items() + ], + "llm": { + "configured": self.llm_configured, + "model": self.llm_primary_model if self.llm_configured else "", + "fallback_configured": self.llm_fallback_configured, + "fallback_model": self.llm_fallback_model if self.llm_fallback_configured else "", + }, + } + + def _heaven_stock_context( + self, + stock_code: str, + trade_date: str, + dashboard: dict[str, Any], + market_mode: str, + ) -> dict[str, Any]: + """Return the only stock contract accepted by heaven trend.""" + pool_row = next( + ( + dict(row) for key in ("limits", "broken", "down_limits") + for row in dashboard.get(key) or [] + if str(row.get("code") or "") == stock_code + ), + {}, + ) + if market_mode == "intraday": + if self.configured: + try: + quote = self._tushare_client().realtime_stock_quote( + tushare_code(stock_code), + trade_date, + ) + return { + **quote, + "status": pool_row.get("status") or "普通", + "seal_amount_million": pool_row.get("seal_amount_million") or 0, + "open_times": pool_row.get("open_times") or 0, + "streak": pool_row.get("streak") or 0, + "precise": True, + } + except TushareError: + pass + if pool_row: + return { + **pool_row, + "data_source": "dashboard_rt" if dashboard.get("meta", {}).get("realtime") else "dashboard", + "trade_date": trade_date, + "realtime": bool(dashboard.get("meta", {}).get("realtime")), + "precise": False, + } + return { + "code": stock_code, + "name": "--", + "sector": "其他", + "trade_date": trade_date, + "realtime": False, + "precise": False, + } + + detail = self.get_stock_detail(stock_code, trade_date, force=True) + detail_meta = detail.get("meta") or {} + stock = detail.get("stock") or {} + resolved_date = normalize_date(str(detail_meta.get("trade_date") or trade_date)) + source = str(detail_meta.get("source") or "") + return { + "code": stock_code, + "name": stock.get("name") or pool_row.get("name") or "--", + "sector": stock.get("industry") or pool_row.get("sector") or "其他", + "status": pool_row.get("status") or "普通", + "change": stock.get("change") or 0, + "turnover_rate": stock.get("turnover_rate") or 0, + "amount_billion": stock.get("amount_billion") or 0, + "seal_amount_million": pool_row.get("seal_amount_million") or 0, + "open_times": pool_row.get("open_times") or 0, + "streak": pool_row.get("streak") or 0, + "data_source": source, + "trade_date": resolved_date, + "realtime": False, + "precise": source == "tushare" and resolved_date == trade_date, + } + + @staticmethod + def _heaven_market_mode( + trade_date: str, + dashboard: dict[str, Any], + now: datetime | None = None, + ) -> str: + """区分盘中、今日收盘和历史,避免把 rt_k 数据来源误当成交易状态。""" + now = now or datetime.now().astimezone() + if trade_date != now.strftime("%Y%m%d"): + return "historical" + meta = dashboard.get("meta") or {} + status = str(meta.get("market_status") or "").lower() + local_time = now.time().replace(tzinfo=None) + if status == "closed" or local_time > datetime.strptime("15:05", "%H:%M").time(): + return "closed" + if status in {"trading", "auction", "pre_open"} or ( + bool(meta.get("realtime")) + and local_time >= datetime.strptime("09:15", "%H:%M").time() + ): + return "intraday" + return "historical" + + @staticmethod + def _heaven_trend_sources( + trade_date: str, + index_context: dict[str, Any], + sector: dict[str, Any] | None, + stock: dict[str, Any] | None, + ) -> list[dict[str, Any]]: + sector = sector or {} + stock = stock or {} + return [ + { + "lines": "五爻、上爻", + "layer": "指数", + "source": index_context.get("source") or "unavailable", + "trade_date": index_context.get("trade_date") or "", + "realtime": bool(index_context.get("realtime")), + "detail": f"三大指数 {len(index_context.get('indices') or [])}/3", + }, + { + "lines": "三爻、四爻", + "layer": "行业", + "source": sector.get("source") or "unavailable", + "trade_date": sector.get("trade_date") or "", + "realtime": bool(sector.get("realtime")), + "detail": ( + f"申万二级 {sector.get('name') or '--'} {sector.get('code') or '--'} " + f"成分覆盖 {int(sector.get('quote_count') or 0)}/{int(sector.get('member_count') or 0)}" + ), + }, + { + "lines": "初爻、二爻", + "layer": "个股", + "source": stock.get("data_source") or "unavailable", + "trade_date": stock.get("trade_date") or trade_date, + "realtime": bool(stock.get("realtime")), + "detail": ( + f"{stock.get('name') or '--'};换手基准 " + f"{stock.get('capital_trade_date') or '--'}" + ), + }, + ] + + @staticmethod + def _heaven_trend_quality_issues( + trade_date: str, + dashboard: dict[str, Any], + index_context: dict[str, Any], + sector: dict[str, Any] | None, + stock: dict[str, Any] | None, + market_mode: str = "historical", + ) -> list[str]: + issues: list[str] = [] + intraday = market_mode == "intraday" + closed = market_mode == "closed" + if intraday: + meta = dashboard.get("meta") or {} + market_status = str(meta.get("market_status") or "") + now = datetime.now().astimezone() + try: + updated_at = datetime.fromisoformat(str(meta.get("updated_at") or "")) + if updated_at.tzinfo is None: + updated_at = updated_at.replace(tzinfo=now.tzinfo) + snapshot_age = (now - updated_at.astimezone(now.tzinfo)).total_seconds() + except ValueError: + snapshot_age = float("inf") + if market_status in {"trading", "auction", "pre_open"} and snapshot_age > 120: + issues.append("主行情快照超过2分钟,请点击顶部刷新") + # 收盘后不再用 dashboard.market_status 作为阻断条件。盘后同步可能将 + # rt_k 快照替换成同日盘后日线而不带该字段;六爻数据本身的日期、 + # 完整性和来源校验已足以判断是否可以成卦。 + + index_date = str(index_context.get("trade_date") or "").replace("-", "") + index_rows = list(index_context.get("indices") or []) + index_row_dates = { + str(row.get("trade_date") or "").replace("-", "") for row in index_rows + } + if not index_context.get("precise") or len(index_rows) < 3: + issues.append("指数层缺少三大指数的有效行情") + elif index_date != trade_date or index_row_dates != {trade_date}: + issues.append("指数行情与目标交易日不一致") + elif intraday and not index_context.get("realtime"): + issues.append("盘中指数层缺少可核验的实时行情") + elif not intraday and ( + index_context.get("realtime") + or str(index_context.get("source") or "") != "tushare" + ): + issues.append("历史/收盘指数层必须使用 Tushare 官方指数日线") + + sector = sector or {} + sector_date = str(sector.get("trade_date") or "").replace("-", "") + sector_coverage = float(sector.get("coverage") or 0) + sector_explained_count = int( + sector.get("explained_count") + if sector.get("explained_count") is not None + else sector.get("quote_count") or 0 + ) + sector_explained_coverage = float( + sector.get("explained_coverage") + if sector.get("explained_coverage") is not None + else sector_coverage + ) + sector_coverage_issue = _sector_coverage_issue( + int(sector.get("member_count") or 0), + int(sector.get("quote_count") or 0), + sector_explained_coverage, + sector_explained_count, + ) + if not sector: + issues.append("行业层缺少申万二级行业归属") + elif sector.get("taxonomy") != "sw_l2": + issues.append("行业层必须使用申万二级行业分类") + elif sector_date != trade_date: + issues.append("行业行情与目标交易日不一致") + elif intraday and not sector.get("realtime"): + issues.append("盘中行业层缺少申万实时行情") + elif market_mode == "historical" and sector.get("realtime"): + issues.append("历史行业层不能使用实时快照") + elif closed and sector.get("realtime") and not sector.get("finalized"): + issues.append("收盘行业层缺少15:00最终快照") + if not sector.get("inner_precise", sector.get("precise")): + issues.append("行业内核缺少可核验的成分行情") + if not sector.get("outer_precise", sector.get("precise")): + issues.append("行业外显缺少申万官方行情") + if sector and sector_coverage_issue: + issues.append(sector_coverage_issue) + if sector.get("realtime") and not sector.get("relative_turnover"): + issues.append("行业内核缺少相对全市场换手活跃度") + + stock = stock or {} + stock_date = str(stock.get("trade_date") or "").replace("-", "") + if not stock or not stock.get("code"): + issues.append("个股层尚未载入有效标的") + elif not stock.get("precise"): + issues.append("个股层缺少可核验的行情数据") + elif stock_date != trade_date: + issues.append("个股行情与目标交易日不一致") + elif intraday and not stock.get("realtime"): + issues.append("盘中个股层不是 rt_k 实时行情") + elif not intraday and ( + stock.get("realtime") + or str(stock.get("data_source") or "") != "tushare" + ): + issues.append("历史/收盘个股层必须使用 Tushare 官方日线") + if intraday and stock and not stock.get("turnover_source"): + issues.append("个股内核缺少可核验的实时换手率") + elif intraday and stock.get("turnover_source") == "unavailable": + issues.append("个股内核缺少流通股本,无法计算实时换手率") + if intraday and stock.get("activity_source") == "unavailable": + issues.append("个股内核缺少近5日量能基准") + elif intraday and not stock.get("activity_source"): + issues.append("个股内核缺少同时间进度量能") + return issues + + def heaven_personal(self, payload: dict[str, Any]) -> dict[str, Any]: + trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) + field = build_five_phase_field( + trade_date, + self.database.list_sector_phase_overrides(), + ) + personal = self.account_personal_field(trade_date, field, public=True) + if not personal: + raise ValueError("请先在账号设置中保存个人命理资料。") + return personal + + def heaven_hexagram(self, raw_lines: Any) -> dict[str, Any]: + if not isinstance(raw_lines, list): + raise ValueError("六爻起卦结果格式不正确。") + try: + lines = [int(value) for value in raw_lines] + except (TypeError, ValueError) as exc: + raise ValueError("六爻必须由六、七、八、九组成。") from exc + return hexagram_from_lines(lines) + + def heaven_readings( + self, mode: str, context_date: str = "", limit: int = 100 + ) -> dict[str, Any]: + mode = str(mode or "").strip() + if mode not in {"trend", "fortune", "heart"}: + raise ValueError("解读记录类型不正确。") + normalized_date = normalize_date(context_date) if context_date else "" + return { + "mode": mode, + "items": self.database.list_heaven_readings( + self.current_user_id, mode, normalized_date, limit + ), + } + + @staticmethod + def _heaven_reading_identity( + mode: str, context_date: str, context: dict[str, Any] + ) -> tuple[str, str]: + display_date = MarketServiceMixin._display_compact_date(context_date) + if mode == "trend": + stock = (context.get("selected_focus") or {}).get("stock") or {} + code = str(stock.get("code") or "").strip() + name = str(stock.get("name") or "").strip() + hexagram = context.get("hexagram") or {} + transformed = hexagram.get("transformed") or {} + subject = " ".join(item for item in (code, name) if item) or "观势" + detail = f"{display_date} · {hexagram.get('name') or '--'} → {transformed.get('name') or '--'}" + return subject, detail + if mode == "fortune": + field = context.get("five_phase_field") or {} + pillars = field.get("pillars") or {} + dominant = (field.get("balance") or [{}])[0] + subject = f"{display_date} 观气" + detail = ( + f"{pillars.get('year') or '--'}年 · {pillars.get('month') or '--'}月 · " + f"{pillars.get('day') or '--'}日 · {dominant.get('element') or '--'}气偏显" + ) + return subject, detail + hexagram = context.get("hexagram") or {} + transformed = hexagram.get("transformed") or {} + return ( + f"{display_date} 观心", + f"{hexagram.get('name') or '--'} → {transformed.get('name') or '--'}", + ) + + def heaven_interpret(self, payload: dict[str, Any]) -> dict[str, Any]: + mode = str(payload.get("mode") or "").strip() + if mode not in {"trend", "fortune", "heart"}: + raise ValueError("问天解读模式不正确。") + trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat())) + if mode == "fortune": + existing = self.database.latest_heaven_reading( + self.current_user_id, "fortune", trade_date + ) + if self._legacy_truncated_heaven_reading(existing): + self.database.delete_heaven_reading( + self.current_user_id, int(existing["id"]) + ) + existing = None + if existing: + return { + "answer": existing["answer"], + "mode": mode, + "compiler": "stored", + "notice": "", + "reading": existing, + "reused": True, + } + if mode in {"trend", "fortune"}: + setup = self.heaven_setup( + trade_date, + str(payload.get("sector") or ""), + str(payload.get("stock_code") or ""), + payload.get("manual_data"), + ) + if mode == "trend": + chart = setup["chart"] + if not chart.get("available"): + issues = ";".join((chart.get("quality") or {}).get("issues") or []) + raise ValueError(f"观势数据未通过六爻校验,暂不解势:{issues}") + hexagram_context = json.loads(json.dumps(chart["hexagram"], ensure_ascii=False)) + for line in hexagram_context.get("lines", []): + line.pop("evidence", None) + line.pop("score", None) + line.pop("talent", None) + line.pop("layer", None) + line.pop("role", None) + if not line.get("moving"): + line.pop("text", None) + line.pop("image", None) + line.pop("line_name", None) + context = { + "data_trade_date": setup["trade_date"], + "selected_focus": { + "sector": chart.get("sector") or "", + "stock": chart.get("stock") or {}, + }, + "hexagram": hexagram_context, + "movement": chart.get("movement") or {}, + } + else: + personal_profile = self.account_personal_field( + setup["calendar_date"], + setup["field"], + public=False, + ) + fortune_field = json.loads(json.dumps(setup["field"], ensure_ascii=False)) + catalog = fortune_field.pop("sector_catalog", []) + dominant_elements = { + item.get("element") for item in fortune_field.get("balance", [])[:2] + } + fortune_field["industry_affinity"] = [ + { + "element": group.get("element"), + "examples": [ + item.get("name") + for item in group.get("industries", [])[:8] + if item.get("name") + ], + } + for group in catalog + if group.get("element") in dominant_elements + ] + context = { + "calendar_date": setup["calendar_date"], + "five_phase_field": fortune_field, + "personal_profile": personal_profile, + } + context_date = setup["calendar_date"] + if mode == "trend": + context_date = setup["trade_date"] + else: + context = { + "hexagram": self.heaven_hexagram(payload.get("lines")), + "ritual": "用户已完成30秒静心、六次三枚铜钱起卦,并在心中察看第一念。问题未输入。", + } + context_date = trade_date + result, compiler = self._call_heaven_agent(mode, context) + subject, subject_detail = self._heaven_reading_identity( + mode, context_date, context + ) + dedupe_key = ( + f"fortune:{context_date}" + if mode == "fortune" + else f"{mode}:{context_date}:{secrets.token_urlsafe(12)}" + ) + reading = self.database.save_heaven_reading( + self.current_user_id, + mode, + context_date, + subject, + subject_detail, + str(result.get("answer") or ""), + context, + dedupe_key, + ) + return { + **result, + "mode": mode, + "compiler": compiler, + "notice": "智能解读已自动切换可用服务。" if compiler == "fallback" else "", + "reading": reading, + "reused": False, + } + + @staticmethod + def _legacy_truncated_heaven_reading(reading: dict[str, Any] | None) -> bool: + return bool(reading and str(reading.get("answer") or "").rstrip().endswith("……")) + + def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]: + result = self.llm_gateway.call( + f"heaven_{mode}", + f"heaven-{mode}-v1", + lambda profile: interpret_heaven( + mode, + context, + profile.api_key, + profile.base_url, + profile.model, + ), + (HeavenAgentError,), + ) + return result.value, result.role + + def _heaven_index_context( + self, + trade_date: str, + dashboard: dict[str, Any], + market_mode: str = "historical", + ) -> dict[str, Any]: + cached = self.database.get_data_snapshot("heaven_indices", trade_date) + cached_valid = False + if cached: + cached_rows = list(cached.get("indices") or []) + cached_dates = { + str(row.get("trade_date") or "").replace("-", "") + for row in cached_rows + } + cached_valid = ( + len(cached_rows) == 3 + and cached_dates == {trade_date} + and bool(cached.get("precise")) + and not cached.get("realtime") + and str(cached.get("source") or "") == "tushare" + and int(cached.get("schema_version") or 0) >= 3 + ) + if market_mode != "intraday" and cached_valid: + return cached + + if not self.configured: + error = "Tushare Token 未配置" + else: + try: + client = self._tushare_client() + if market_mode == "intraday": + payload = self._aggregate_index_context(trade_date) + payload["schema_version"] = 3 + return payload + payload = client.market_indices(trade_date) + payload["schema_version"] = 3 + if market_mode == "closed": + payload["finalized"] = True + self.database.save_data_snapshot( + "heaven_indices", + trade_date, + str(payload.get("source") or "tushare"), + payload, + ) + return payload + except Exception as exc: + error = str(exc) + overview = dashboard.get("overview") or {} + up_count = float(overview.get("up_count") or 0) + down_count = float(overview.get("down_count") or 0) + breadth = (up_count - down_count) / max(up_count + down_count, 1) + return { + "source": "market_breadth_proxy", + "trade_date": trade_date, + "realtime": False, + "precise": False, + "schema_version": 3, + "notice": f"指数数据不可用,当前以市场宽度代理:{error}", + "indices": [], + "aggregate": { + "average_pct_chg": round(breadth * 2.5, 3), + "average_return_5d": 0, + "average_return_20d": 0, + }, + } + + def _aggregate_index_context( + self, + trade_date: str, + tushare_error: str = "", + ) -> dict[str, Any]: + quotes = self.realtime_aggregator.tencent_indices() + epochs = [int(item.get("quote_time_epoch") or 0) for item in quotes] + quote_dates = { + datetime.fromtimestamp(epoch).astimezone().strftime("%Y%m%d") + for epoch in epochs if epoch + } + if len(quotes) != 3 or quote_dates != {trade_date}: + raise ValueError("腾讯三大指数日期与目标交易日不一致") + now = datetime.now().astimezone() + max_skew = 120 if now.hour >= 15 else 15 + if max(epochs) - min(epochs) > max_skew: + raise ValueError(f"腾讯三大指数时间差超过{max_skew}秒") + + code_map = { + "000001": "000001.SH", + "399001": "399001.SZ", + "399006": "399006.SZ", + } + client = self._tushare_client() + indices = [] + start_date = ( + datetime.strptime(trade_date, "%Y%m%d") - timedelta(days=20) + ).strftime("%Y%m%d") + for quote in quotes: + ts_code = code_map[str(quote.get("code") or "")] + history = client.query( + "index_daily", + {"ts_code": ts_code, "start_date": start_date, "end_date": trade_date}, + "ts_code,trade_date,close,pct_chg", + ) + history.sort(key=lambda item: str(item.get("trade_date") or "")) + completed_closes = [ + float(item.get("close") or 0) + for item in history + if str(item.get("trade_date") or "") < trade_date + and float(item.get("close") or 0) > 0 + ] + close_5d = ( + completed_closes[-5] + if len(completed_closes) >= 5 + else completed_closes[0] if completed_closes else 0 + ) + close = float(quote.get("price") or 0) + indices.append( + { + "ts_code": ts_code, + "name": quote.get("name") or ts_code, + "trade_date": trade_date, + "close": close, + "pct_chg": round(float(quote.get("change") or 0), 3), + "return_5d": round((close / close_5d - 1) * 100, 3) if close_5d else 0, + "return_20d": 0, + "amount_billion": float(quote.get("amount_billion") or 0), + "quote_time": quote.get("quote_time") or "", + } + ) + return { + "trade_date": trade_date, + "source": "+".join( + sorted({str(item.get("source") or "web_quote") for item in quotes}) + + ["tushare_index_daily"] + ), + "realtime": True, + "precise": True, + "indices": indices, + "aggregate": { + "average_pct_chg": round( + sum(item["pct_chg"] for item in indices) / len(indices), 3 + ), + "average_return_5d": round( + sum(item["return_5d"] for item in indices) / len(indices), 3 + ), + "average_return_20d": 0, + }, + "quote_time_skew_seconds": max(epochs) - min(epochs), + "notice": ( + "指数实时行情来自腾讯行情,5日趋势来自Tushare历史指数。" + + (f" Tushare实时指数未使用:{tushare_error}" if tushare_error else "") + ), + } + + def _heaven_sector_context( + self, + identifier: str, + trade_date: str, + market_mode: str = "historical", + ) -> dict[str, Any] | None: + """Return the Shenwan L2 sector context for heaven trend. + + 观势行业层只使用申万二级行业。外显盘中使用 rt_sw_k、历史使用 + sw_daily;内核独立使用目标日期成分股行情聚合。收盘过渡期在 + sw_daily 入库前接受同日15:00后的 rt_sw_k 收盘快照。 + """ + cache_key = f"{trade_date}:{identifier.strip().lower()}" + cached = self.database.get_data_snapshot("heaven_sector", cache_key) + cached_date = str((cached or {}).get("trade_date") or "").replace("-", "") + cached_valid = bool( + cached + and cached_date == trade_date + and cached.get("taxonomy") == "sw_l2" + and cached.get("inner_precise", cached.get("precise")) + and cached.get("outer_precise", cached.get("precise")) + and not cached.get("realtime") + and int(cached.get("schema_version") or 0) >= 6 + ) + if market_mode != "intraday" and cached_valid: + return cached + if not self.configured: + return None + try: + payload = self._tushare_client().sw_sector_snapshot( + tushare_code(identifier), + trade_date, + realtime_expected=market_mode == "intraday", + allow_realtime_close=market_mode == "closed", + ) + except TushareError as exc: + if cached_valid: + return cached + return { + "name": "", + "code": "", + "taxonomy": "sw_l2", + "source": "tushare", + "trade_date": trade_date, + "realtime": market_mode == "intraday", + "precise": False, + "inner_precise": False, + "outer_precise": False, + "coverage": 0, + "member_count": 0, + "quote_count": 0, + "error": f"申万二级行业数据获取失败:{exc}", + } + if not payload.get("realtime") and payload.get("precise"): + self.database.save_data_snapshot( + "heaven_sector", + cache_key, + str(payload.get("source") or "tushare"), + payload, + ) + return payload diff --git a/app/database.py b/app/database.py index d55cc9a..2f75274 100644 --- a/app/database.py +++ b/app/database.py @@ -10,6 +10,7 @@ from backend.database import MIGRATIONS, MigrationRunner, SQLiteConnectionFactor from backend.features.accounts.repository import AccountRepositoryMixin from backend.features.auction.repository import AuctionRepositoryMixin from backend.features.dragon_tiger.repository import DragonTigerRepositoryMixin +from backend.features.heaven.repository import HeavenRepositoryMixin from backend.features.market.repository import MarketRepositoryMixin from backend.features.mentor.repository import MentorRepositoryMixin from backend.features.pools.repository import PoolRepositoryMixin @@ -23,6 +24,7 @@ class ReviewDatabase( AccountRepositoryMixin, AuctionRepositoryMixin, DragonTigerRepositoryMixin, + HeavenRepositoryMixin, MarketRepositoryMixin, MentorRepositoryMixin, PoolRepositoryMixin, @@ -1121,106 +1123,3 @@ class ReviewDatabase( "DELETE FROM assistant_messages WHERE user_id = ?", (int(user_id),) ) return int(cursor.rowcount) - - @staticmethod - def _heaven_reading_dict(row: sqlite3.Row | None) -> dict[str, Any] | None: - if not row: - return None - return { - "id": int(row["id"]), - "mode": str(row["mode"]), - "context_date": str(row["context_date"]), - "subject": str(row["subject"]), - "subject_detail": str(row["subject_detail"]), - "answer": str(row["answer"]), - "created_at": str(row["created_at"]), - } - - def save_heaven_reading( - self, - user_id: int, - mode: str, - context_date: str, - subject: str, - subject_detail: str, - answer: str, - context_snapshot: dict[str, Any], - dedupe_key: str, - ) -> dict[str, Any]: - now = datetime.now().astimezone().isoformat(timespec="seconds") - snapshot_json = json.dumps( - context_snapshot, ensure_ascii=False, separators=(",", ":") - ) - with self.connect() as connection: - connection.execute( - """ - INSERT INTO heaven_readings - (user_id, mode, context_date, subject, subject_detail, answer, - context_snapshot, dedupe_key, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(user_id, dedupe_key) DO NOTHING - """, - ( - int(user_id), mode, context_date, subject, subject_detail, - answer, snapshot_json, dedupe_key, now, - ), - ) - row = connection.execute( - """ - SELECT id, mode, context_date, subject, subject_detail, answer, created_at - FROM heaven_readings WHERE user_id = ? AND dedupe_key = ? - """, - (int(user_id), dedupe_key), - ).fetchone() - connection.execute( - """ - DELETE FROM heaven_readings - WHERE user_id = ? AND mode = ? AND id NOT IN ( - SELECT id FROM heaven_readings - WHERE user_id = ? AND mode = ? ORDER BY id DESC LIMIT 100 - ) - """, - (int(user_id), mode, int(user_id), mode), - ) - result = self._heaven_reading_dict(row) - if not result: - raise ValueError("解读记录保存失败。") - return result - - def list_heaven_readings( - self, - user_id: int, - mode: str, - context_date: str = "", - limit: int = 100, - ) -> list[dict[str, Any]]: - clauses = ["user_id = ?", "mode = ?"] - parameters: list[Any] = [int(user_id), mode] - if context_date: - clauses.append("context_date = ?") - parameters.append(context_date) - parameters.append(max(1, min(100, int(limit)))) - with self.connect() as connection: - rows = connection.execute( - f""" - SELECT id, mode, context_date, subject, subject_detail, answer, created_at - FROM heaven_readings WHERE {' AND '.join(clauses)} - ORDER BY context_date DESC, id DESC LIMIT ? - """, - parameters, - ).fetchall() - return [self._heaven_reading_dict(row) for row in rows if row] - - def latest_heaven_reading( - self, user_id: int, mode: str, context_date: str = "" - ) -> dict[str, Any] | None: - items = self.list_heaven_readings(user_id, mode, context_date, 1) - return items[0] if items else None - - def delete_heaven_reading(self, user_id: int, reading_id: int) -> bool: - with self.connect() as connection: - cursor = connection.execute( - "DELETE FROM heaven_readings WHERE id = ? AND user_id = ?", - (int(reading_id), int(user_id)), - ) - return cursor.rowcount > 0 diff --git a/app/heaven_agent.py b/app/heaven_agent.py index e4d5fcc..ebef605 100644 --- a/app/heaven_agent.py +++ b/app/heaven_agent.py @@ -1,118 +1,7 @@ -from __future__ import annotations +"""Compatibility alias for the canonical heaven agent implementation.""" -import json -import time -import urllib.error -import urllib.request -from typing import Any +import sys +from backend.features.heaven import agent as _implementation -class HeavenAgentError(RuntimeError): - pass - - -def interpret_heaven( - mode: str, - context: dict[str, Any], - api_key: str, - base_url: str, - model: str, - timeout: int = 90, -) -> dict[str, Any]: - if mode not in {"trend", "fortune", "heart"}: - raise HeavenAgentError("不支持的问天解读模式。") - if not api_key or not model: - raise HeavenAgentError("LLM API Key 或模型尚未配置。") - system_prompt = _system_prompt(mode) - payload = json.dumps( - { - "model": model, - "messages": [ - {"role": "system", "content": system_prompt}, - { - "role": "user", - "content": json.dumps(context, ensure_ascii=False, separators=(",", ":")), - }, - ], - "stream": False, - }, - ensure_ascii=False, - ).encode("utf-8") - request = urllib.request.Request( - f"{base_url.rstrip('/')}/chat/completions", - data=payload, - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}", - "User-Agent": "XiaobaiReviewWeb/0.7", - }, - method="POST", - ) - started = time.perf_counter() - try: - with urllib.request.urlopen(request, timeout=timeout) as response: - result = json.loads(response.read().decode("utf-8")) - answer = str(result["choices"][0]["message"]["content"]).strip() - if not answer: - raise KeyError("empty response") - except urllib.error.HTTPError as exc: - raise HeavenAgentError(_http_error_message(exc)) from exc - except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as exc: - raise HeavenAgentError(f"问天模型调用失败:{exc}") from exc - return { - "answer": answer, - "model": model, - "latency_ms": round((time.perf_counter() - started) * 1000), - } - - -def _system_prompt(mode: str) -> str: - common = """ -你是“小白复盘”的问天解读器。所有历法、卦象、爻位和市场指标已经由确定性程序计算,你只能解释提供的数据,不得改卦、改爻、改干支或编造行情。 -问天属于传统文化与娱乐化观察,不是预测模型,不承诺应验,不输出无条件买卖指令,不用神秘话术制造确定性。 -使用中文,先给核心判断,再解释结构。引用市场数字时标明数据日期。输出纯文本,可使用简短标题。 -""".strip() - if mode == "trend": - return common + """ - -当前任务是“观势·解势”。六爻从初爻到上爻依次是个股内核、个股外显、板块内核、板块外显、指数内核、指数外显;初二为地、三四为人、五上为天。 -行情数据只负责生成六爻,本次解势必须以卦象本身为主,不得根据指数涨跌、板块强弱、涨停家数、成交量或个股表现直接推演方向。context中不会提供这些数字,也不会提供爻位对应的市场角色。 -先解释本卦卦名的核心义、上下卦组合及大象;再只解释实际动爻所代表的转折,并说明本卦如何走向之卦;最后可把这一组卦势翻译成克制的市场语言。 -重点是“本卦为当下之势,动爻为变化关节,之卦为所趋之势”。不要说明某一动爻对应指数、板块或个股,也不要输出“一看指数、二看涨停家数”一类行情观察条件。 -全文控制在300至450个中文字符,最多四小段。卦理约占九成,市场翻译最多一句,只能落到节制、等待、守信、辨伪等行为态度,不得据此预测市场下一阶段、涨跌方向或动能变化。不直接荐股,不使用Markdown表格。 -不要使用“必然、确定、必涨、必跌、后续将、进入某阶段”等断语;天机只点出势的性质与变化关系,不替用户宣布结果。 -""".strip() - if mode == "fortune": - return common + """ - -当前任务是“观气·解运”。严格区分五运、六气、节气、月令和日干,不把丙午简单解释为火年。 -严格服从five_phase_field.framework提供的确定性结构,不自行重新计算五行:年纲由中运与司天在泉构成;岁半以前司天为主、在泉为辅,岁半以后在泉为主、司天为辅;当前六气层以客气加临主气为核心;日辰只负责触发。节气只用于定位当前六气阶段,不得再次叠加为独立力量。 -重点解释framework.relations中的客主同气、客生主、主生客、客克主或主克客,以及客胜为从、主胜为逆、司天在泉同位、天符岁会等已经判定的关系。不得把司天、在泉、主气、客气视为彼此独立的证据重复计权,也不得自行增删传统格局。 -首要解释当日气场容易放大参与者的哪些情绪、判断偏差和操作冲动,例如急躁、恐惧、迟疑、追涨、过早止损或路径依赖;再给出一至两个调节动作。 -如有personal_profile,结合其日主、十神、五行平衡倾向说明当日对该用户主观状态的影响,但不得把简化平衡倾向说成唯一喜用神,也不得复述或猜测出生日期。 -不得引用市场上涨下跌家数、涨跌停数量、成交额、板块强度或个股表现来证明气场。industry_affinity只是五行行业取象示例,不是行情旁证;行业契合度最多在末尾用一句话说明,不得写“当日共振”或暗示相关行业必然涨跌。 -全文控制在420至600个中文字符,按“三层气机、人的状态、操作偏向、个人影响(如有)、制衡动作”组织,标题必须写“三层气机”。明确这些是传统历法框架下的观察语言,不宣称气候或五行直接导致股价。 -""".strip() - return common + """ - -当前任务是“观心·解卦”。用户的问题始终只在心中,没有输入给你,因此你不能猜测问题内容,也不能替用户作具体决定。 -全文控制在180至350个中文字符。只写一句卦意;一小段动爻与之卦;最后三句极短的问心句。 -不要重述六条爻辞,不猜用户未说出口的问题,不以吉凶二字替代思考,不给出股票涨跌预测。语气安静、克制,越短越有余味。 -""".strip() - - -def _http_error_message(exc: urllib.error.HTTPError) -> str: - detail = "" - try: - payload = json.loads(exc.read().decode("utf-8", errors="replace")) - error = payload.get("error") - if isinstance(error, dict): - detail = str(error.get("message") or error.get("code") or "") - elif error: - detail = str(error) - elif payload.get("message"): - detail = str(payload["message"]) - except (json.JSONDecodeError, OSError): - detail = "" - suffix = f":{detail[:300]}" if detail else "" - return f"问天模型调用失败(HTTP {exc.code}){suffix}" +sys.modules[__name__] = _implementation diff --git a/app/heaven_engine.py b/app/heaven_engine.py index 9545d47..66b9f30 100644 --- a/app/heaven_engine.py +++ b/app/heaven_engine.py @@ -1,1182 +1,7 @@ -from __future__ import annotations +"""Compatibility alias for the canonical heaven engine implementation.""" -import json -import math import sys -from datetime import datetime -from functools import lru_cache -from pathlib import Path -from typing import Any +from backend.features.heaven import engine as _implementation -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 +sys.modules[__name__] = _implementation diff --git a/app/tests/test_market_mode.py b/app/tests/test_market_mode.py index 8e6af49..5fdd5a0 100644 --- a/app/tests/test_market_mode.py +++ b/app/tests/test_market_mode.py @@ -9,11 +9,11 @@ from tushare_client import _sector_coverage_issue def load_method(name: str): - source = Path("backend/application.py").read_text(encoding="utf-8") + source = Path("backend/features/heaven/service.py").read_text(encoding="utf-8") tree = ast.parse(source) dashboard_service = next( node for node in tree.body - if isinstance(node, ast.ClassDef) and node.name == "DashboardService" + if isinstance(node, ast.ClassDef) and node.name == "HeavenServiceMixin" ) method = next( node for node in dashboard_service.body diff --git a/app/tests/test_preservation_slice_heaven.py b/app/tests/test_preservation_slice_heaven.py new file mode 100644 index 0000000..8c94d0e --- /dev/null +++ b/app/tests/test_preservation_slice_heaven.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import ast +import hashlib +import unittest +from pathlib import Path + +import heaven_agent +import heaven_engine +from backend.features.heaven import agent as canonical_agent +from backend.features.heaven import engine as canonical_engine + + +APP_ROOT = Path(__file__).resolve().parents[1] +ORIGINAL_ROOT = APP_ROOT.parent + +HEAVEN_SERVICE_METHODS = { + "_heaven_manual_schema", + "_validate_heaven_manual_data", + "_apply_heaven_manual_data", + "_heaven_line_checks", + "_resolve_heaven_stock_code", + "heaven_setup", + "_heaven_stock_context", + "_heaven_market_mode", + "_heaven_trend_sources", + "_heaven_trend_quality_issues", + "heaven_personal", + "heaven_hexagram", + "heaven_readings", + "_heaven_reading_identity", + "heaven_interpret", + "_legacy_truncated_heaven_reading", + "_call_heaven_agent", + "_heaven_index_context", + "_aggregate_index_context", + "_heaven_sector_context", +} + +HEAVEN_REPOSITORY_METHODS = { + "_heaven_reading_dict", + "save_heaven_reading", + "list_heaven_readings", + "latest_heaven_reading", + "delete_heaven_reading", +} + +HEAVEN_HTTP_METHODS = {"heaven_hexagram", "heaven_personal", "heaven_interpret"} + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def class_methods(path: Path, class_name: str) -> dict[str, str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + owner = next( + node + for node in tree.body + if isinstance(node, ast.ClassDef) and node.name == class_name + ) + return { + node.name: ast.dump(node, include_attributes=False) + for node in owner.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + + +def top_level_definitions(path: Path) -> dict[str, str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + return { + node.name: ast.dump(node, include_attributes=False) + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + } + + +class HeavenSliceSourceEquivalenceTests(unittest.TestCase): + def assert_methods_equal( + self, + original_path: Path, + original_class: str, + migrated_path: Path, + migrated_class: str, + expected: set[str], + adapted: set[str] | None = None, + ) -> None: + original = class_methods(original_path, original_class) + migrated = class_methods(migrated_path, migrated_class) + self.assertEqual(set(migrated), expected) + for name in sorted(expected - (adapted or set())): + self.assertEqual(migrated[name], original[name], name) + + def test_heaven_agent_is_an_exact_file(self) -> None: + self.assertEqual( + sha256(ORIGINAL_ROOT / "heaven_agent.py"), + sha256(APP_ROOT / "backend" / "features" / "heaven" / "agent.py"), + ) + + def test_heaven_engine_definitions_are_exact_original_ast(self) -> None: + self.assertEqual( + top_level_definitions(ORIGINAL_ROOT / "heaven_engine.py"), + top_level_definitions( + APP_ROOT / "backend" / "features" / "heaven" / "engine.py" + ), + ) + + def test_compatibility_modules_are_canonical_module_objects(self) -> None: + self.assertIs(heaven_agent, canonical_agent) + self.assertIs(heaven_engine, canonical_engine) + + def test_heaven_service_methods_are_exact_original_ast(self) -> None: + self.assert_methods_equal( + ORIGINAL_ROOT / "server.py", + "DashboardService", + APP_ROOT / "backend" / "features" / "heaven" / "service.py", + "HeavenServiceMixin", + HEAVEN_SERVICE_METHODS, + {"_heaven_reading_identity"}, + ) + source = ( + APP_ROOT / "backend" / "features" / "heaven" / "service.py" + ).read_text(encoding="utf-8") + self.assertIn( + "MarketServiceMixin._display_compact_date(context_date)", source + ) + + def test_heaven_repository_methods_are_exact_original_ast(self) -> None: + self.assert_methods_equal( + ORIGINAL_ROOT / "database.py", + "ReviewDatabase", + APP_ROOT / "backend" / "features" / "heaven" / "repository.py", + "HeavenRepositoryMixin", + HEAVEN_REPOSITORY_METHODS, + ) + + def test_original_classes_no_longer_duplicate_moved_methods(self) -> None: + remaining_service = class_methods( + APP_ROOT / "backend" / "application.py", "DashboardService" + ) + remaining_database = class_methods(APP_ROOT / "database.py", "ReviewDatabase") + remaining_http = class_methods( + APP_ROOT / "backend" / "application.py", "RequestHandler" + ) + self.assertTrue(HEAVEN_SERVICE_METHODS.isdisjoint(remaining_service)) + self.assertTrue(HEAVEN_REPOSITORY_METHODS.isdisjoint(remaining_database)) + self.assertTrue(HEAVEN_HTTP_METHODS.isdisjoint(remaining_http)) + + def test_http_mixin_preserves_all_heaven_endpoints(self) -> None: + methods = class_methods( + APP_ROOT / "backend" / "features" / "heaven" / "http.py", + "HeavenHttpMixin", + ) + self.assertEqual(set(methods), HEAVEN_HTTP_METHODS) + source = ( + APP_ROOT / "backend" / "features" / "heaven" / "http.py" + ).read_text(encoding="utf-8") + self.assertNotIn("SERVICE.", source) + self.assertEqual(source.count("self.application_service.heaven_"), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/migration/evidence/slice-08/README.md b/docs/migration/evidence/slice-08/README.md new file mode 100644 index 0000000..cbfe325 --- /dev/null +++ b/docs/migration/evidence/slice-08/README.md @@ -0,0 +1,69 @@ +# 切片 08:问天、观势、观气与观心 + +> 基线:`2919229`(切片 07) +> 回档标签:`xiaobai-preservation-slice-08-20260731` +> 结论:源码、API、数据库、真实页面、动画与全量回归通过;最终视觉仍等待全站人工验收 + +## 1. 原实现归位 + +本切片只移动原版问天 Agent、历法/卦象引擎、服务、持久化与三个 HTTP 入口,没有从 +`next/`取用代码,也没有改写六爻、安全门、五运六气、个人合参、铜钱起卦、历史去重或 +LLM 解读逻辑。 + +| 原位置 | 新的唯一实现位置 | 兼容方式 | +|---|---|---| +| `app/heaven_agent.py` | `app/backend/features/heaven/agent.py` | 根级模块指向同一模块对象 | +| `app/heaven_engine.py` | `app/backend/features/heaven/engine.py` | 根级模块指向同一模块对象 | +| `DashboardService`问天方法 | `app/backend/features/heaven/service.py` | `HeavenServiceMixin` | +| 问天历史持久化 | `app/backend/features/heaven/repository.py` | `HeavenRepositoryMixin` | +| 三个问天 POST 入口 | `app/backend/features/heaven/http.py` | `HeavenHttpMixin` | + +历法引擎搬迁后仍从`app/vendor`及`app/data/iching_zh.json`读取原资源;这是唯一资源路径适配, +顶层函数与类定义未变。旧测试依赖的根级模块继续有效。 + +## 2. 源码等价 + +- `heaven_agent.py`与原版文件 SHA-256 一致。 +- `heaven_engine.py`所有顶层函数和类与原版无位置信息 AST 一致。 +- 20个问天服务方法中19个与原版 AST 一致;`_heaven_reading_identity`仅将旧巨型类名 + `DashboardService`替换为已归位的`MarketServiceMixin`静态日期格式化方法。 +- 5个问天 Repository 方法与原版 AST 一致。 +- 三个 HTTP 方法只把全局`SERVICE`改为 Mixin 的`self.application_service`,状态码、字段和异常语义不变。 +- `DashboardService`、`ReviewDatabase`与`RequestHandler`不再重复保留已迁移实现。 + +## 3. API 与数据库差分 + +- 原版`8788`和迁移版`8789`使用同一正式数据库的两个临时副本。 +- 固定日期的问天初始化、三类历史读取、六爻成卦和个人五行计算共6个接口,状态码与业务 JSON 完全一致。 +- 未调用真实`/api/heaven/interpret`,避免外部模型波动及测试记录写入;提示词和调用链由源码等价与单元测试覆盖。 +- 两版均为62个 schema 对象;`heaven_readings`、`users`、`system_settings`、`llm_usage`、 + `user_birth_profiles`和`sector_phase_overrides`逐行一致。 +- 差分只在系统临时目录副本运行,正式数据库未写入问天记录或 LLM 用量。 + +## 4. 真实浏览器与动画检查 + +- 1920×1080视口逐页检查观势、观气、观心,无横向溢出,页面滚动边界可用。 +- 观势保留星空、三才六爻与八卦待载入场景;观气生成100个星点,五运六气环动画名为`wt-spin`, + 五行行业默认折叠;观心呼吸波纹、香火燃烧和阶段文字均持续运行。 +- 夜间模式问天背景为`rgb(11, 17, 32)`,全页无控制台错误。 +- 没有触发真实解势、解运或解卦调用,加载动画的静态资产及脚本未改动。 +- 截图 SHA-256: + - `heaven-trend-1080.png`:`9e70d9cbfd741e74d3c4511c0e76ef8e625a6a31fd5d6bde014103f807bcc76e` + - `heaven-fortune-1080.png`:`e4cdf0e0fc1d9325724654a781febed1d509e49ddccecb3355803b5e9f1d2f5c` + - `heaven-heart-breathing-1080.png`:`3493a59b9032c1a8a1ba7da609b499c736170d2c7947b3aa0ac4c8b95d981ff5` + - `heaven-heart-dark-1080.png`:`3d346fea6b593fa4e43037f5fc68eda1a3f3446a223c2022d9084922f646a088` + +## 5. 自动验证与保留边界 + +| 验证 | 结果 | +|---|---:| +| 原版`python -m unittest discover -s tests -q` | 231项通过 | +| 迁移版`python -m unittest discover -s tests -q` | 280项通过 | +| `python -m unittest tests.test_preservation_slice_heaven -q` | 7项通过 | +| `npx.cmd playwright test --reporter=dot` | 45项通过(2.2分钟) | +| `git diff --check` | 通过 | + +- 复盘助手、交易日志、自选、笔记与提醒属于切片09,本切片不提前移动。 +- 前端 DOM、页面 JS、CSS和移动端行为未改动,统一归档延至切片10。 +- `static/heaven-loading.js`是否失效仍不确定,继续保留到切片11试删。 +- 没有删除待定代码、没有修改正式数据库、没有测试或切换 Docker/NAS。 diff --git a/docs/migration/evidence/slice-08/api-diff.json b/docs/migration/evidence/slice-08/api-diff.json new file mode 100644 index 0000000..bfd0941 --- /dev/null +++ b/docs/migration/evidence/slice-08/api-diff.json @@ -0,0 +1,71 @@ +{ + "all_equal": true, + "endpoints": [ + { + "name": "heaven setup awaiting stock selection", + "method": "GET", + "endpoint": "/api/heaven/setup?trade_date=2026-07-29", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "b72f2e972c3993717d639d1ccc68a9f1fb716a5846823a485a05458103465b92", + "migrated_sha256": "b72f2e972c3993717d639d1ccc68a9f1fb716a5846823a485a05458103465b92", + "equal": true, + "first_difference": null + }, + { + "name": "trend interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=trend&limit=20", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "2aa83ea177bd5cb2351ac4f3a60fdcd17e02cfc14b5753e157e515f03fc2c6c1", + "migrated_sha256": "2aa83ea177bd5cb2351ac4f3a60fdcd17e02cfc14b5753e157e515f03fc2c6c1", + "equal": true, + "first_difference": null + }, + { + "name": "fortune interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=fortune&limit=20", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "e60debaad54c58ad34fc7f025bfeaba7d2349aee2531b3f507c6e1968dbdcbf0", + "migrated_sha256": "e60debaad54c58ad34fc7f025bfeaba7d2349aee2531b3f507c6e1968dbdcbf0", + "equal": true, + "first_difference": null + }, + { + "name": "heart interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=heart&limit=20", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "1fe13531909c83c645b4fbb6740f46f2590adb346ab6e43414023967bb51b072", + "migrated_sha256": "1fe13531909c83c645b4fbb6740f46f2590adb346ab6e43414023967bb51b072", + "equal": true, + "first_difference": null + }, + { + "name": "six-line hexagram calculation", + "method": "POST", + "endpoint": "/api/heaven/hexagram", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "9773a9f288ea5fbe063b778fc47c613972fd7747f8fc6f80333316f33923c960", + "migrated_sha256": "9773a9f288ea5fbe063b778fc47c613972fd7747f8fc6f80333316f33923c960", + "equal": true, + "first_difference": null + }, + { + "name": "personal five-phase calculation", + "method": "POST", + "endpoint": "/api/heaven/personal", + "original_status": 200, + "migrated_status": 200, + "original_sha256": "3b179ea4e3e0b43d12e929b4057a51264772fadcf41e5c87573dc5d18873bc9e", + "migrated_sha256": "3b179ea4e3e0b43d12e929b4057a51264772fadcf41e5c87573dc5d18873bc9e", + "equal": true, + "first_difference": null + } + ] +} diff --git a/docs/migration/evidence/slice-08/api-requests.json b/docs/migration/evidence/slice-08/api-requests.json new file mode 100644 index 0000000..9e94377 --- /dev/null +++ b/docs/migration/evidence/slice-08/api-requests.json @@ -0,0 +1,42 @@ +[ + { + "name": "heaven setup awaiting stock selection", + "method": "GET", + "endpoint": "/api/heaven/setup?trade_date=2026-07-29", + "payload": null + }, + { + "name": "trend interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=trend&limit=20", + "payload": null + }, + { + "name": "fortune interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=fortune&limit=20", + "payload": null + }, + { + "name": "heart interpretation history", + "method": "GET", + "endpoint": "/api/heaven/readings?mode=heart&limit=20", + "payload": null + }, + { + "name": "six-line hexagram calculation", + "method": "POST", + "endpoint": "/api/heaven/hexagram", + "payload": { + "lines": [7, 8, 9, 6, 7, 8] + } + }, + { + "name": "personal five-phase calculation", + "method": "POST", + "endpoint": "/api/heaven/personal", + "payload": { + "trade_date": "2026-07-29" + } + } +] diff --git a/docs/migration/evidence/slice-08/database-diff.json b/docs/migration/evidence/slice-08/database-diff.json new file mode 100644 index 0000000..6258c71 --- /dev/null +++ b/docs/migration/evidence/slice-08/database-diff.json @@ -0,0 +1,59 @@ +{ + "all_equal": true, + "schema": { + "object_count": 62, + "original_sha256": "60a4e044f0ddb6502e44b45f65bedc1a0a31d4bd596f386d4ccfe153a6c8ddd1", + "migrated_sha256": "60a4e044f0ddb6502e44b45f65bedc1a0a31d4bd596f386d4ccfe153a6c8ddd1", + "equal": true + }, + "tables": [ + { + "table": "heaven_readings", + "original_count": 31, + "migrated_count": 31, + "original_sha256": "e6cfc86f010fb182d7522bb99e5d268e826f93896035172120df51a011590951", + "migrated_sha256": "e6cfc86f010fb182d7522bb99e5d268e826f93896035172120df51a011590951", + "equal": true + }, + { + "table": "users", + "original_count": 3, + "migrated_count": 3, + "original_sha256": "4a0f135bef8ebae454693d3f40e1157d84d814d18d849f33e2e760ed1d8a7f89", + "migrated_sha256": "4a0f135bef8ebae454693d3f40e1157d84d814d18d849f33e2e760ed1d8a7f89", + "equal": true + }, + { + "table": "system_settings", + "original_count": 1, + "migrated_count": 1, + "original_sha256": "86d333a5c7feaf7111cd79b2db29513326a9b0d3781607f78bf633579e2ca8e9", + "migrated_sha256": "86d333a5c7feaf7111cd79b2db29513326a9b0d3781607f78bf633579e2ca8e9", + "equal": true + }, + { + "table": "llm_usage", + "original_count": 72, + "migrated_count": 72, + "original_sha256": "03a070316a125cef904bbfb2bb06b06e792242d5713142e70354402c741393c3", + "migrated_sha256": "03a070316a125cef904bbfb2bb06b06e792242d5713142e70354402c741393c3", + "equal": true + }, + { + "table": "user_birth_profiles", + "original_count": 1, + "migrated_count": 1, + "original_sha256": "2a74d4b2edd3a730a46f17fb6ab3926575c42662c1d968643b0d54586ab68b13", + "migrated_sha256": "2a74d4b2edd3a730a46f17fb6ab3926575c42662c1d968643b0d54586ab68b13", + "equal": true + }, + { + "table": "sector_phase_overrides", + "original_count": 0, + "migrated_count": 0, + "original_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "migrated_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "equal": true + } + ] +} diff --git a/docs/migration/evidence/slice-08/heaven-fortune-1080.png b/docs/migration/evidence/slice-08/heaven-fortune-1080.png new file mode 100644 index 0000000000000000000000000000000000000000..62ad91c80e577318e256564171dc289843e58709 GIT binary patch literal 107431 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�b7KQq$$gxA)qtbhoK)|wEkoFjReNY1mCRZ?!-O)XhX=Zlzwjpy% ztB+TX9uj>(P=(9B?#ppO-Ze(H9a_zi8VqW%7Xzq z>tLxD<0`fQPeE|oL*x(_>qWUmlCPw8%ihCCngkGtk8{8^C}S#^H@pewXmZ`s6!&CA zs-bofi@qZf!x9of6U1ov%ztdPMR=0j>|BwTRhdk{*viF)*zie{&x%i_)Mk~0m4WB3 zU4~_m%%ojNB@#W5sW=C+NQM%8Q(B>oZNv>?qYxvXVOms9^oMqC;3RF+UdiVSdibAm zBuX|+MJq)EdX#zRGHJFFWEWr0*#Y5;M`rt_t^~M4Mn7-FTz$$bDb8E^r5ei}5w{$7 z!~4~>#RI**WGjFCiG+EX(p>22zGYZPuh7ch1ty(HrLI4Q~D6J4eNFbm< z=$kDF2nYzFk*94LaR$k}yfGiLyOw1+O(V(Y&&Q8k7&bMUh4Avc*n$xzICHL|Ic512ZR(Co_<%d%R2Sx+}B*R?6$0It|DQ5 zbbiP^5iB1NDtSIwy6fYe-#&!s{FZyfb#(mj@6wO|pPo*o>PXPCPQG+GT=75KEU2>~ z>mJvob~Miavv>a2J|IN*??&cxPt}2iuv`Dpj&6giBnw#}bS5igB~awzt}-PkAc;sj zaLqYuIk(-d^8B&$q~Co7l_+nYC>04^l|Ar#$Mm9vedkBp{nYUfp&cE~?IJ-j5PP7i zbhq=Mz@fj^$UvWejp5(xUhbbiAfOIVdH#HG;OKEaE<<|53hv@cWAd1VZno zJ3BbyG(BB-)Xsd;4>4zbT^`GvMdwe;rnd7_M@fz*2;4W&IGC()Mn z#_?k)RF}>ITU9vz?rhpbph0e_r;#I<@p3V>i2QbtDMOa?k;4Zsyxw?pQ)-5WILVsam%il5|Qrw+_9@$SgsOAyg&w|EX(v^ zz{udaxKpT#;l7)BB1c3kw|m^@=Q*GFqo%hkZN^?}rsA9&SDN+HWR;iMg6YyTas;v2 zTWZ7#?OWrht?q};3j@@@U~o&B3aKp5Nn*Lr~bFPVnZ|0QK; zchXy*gv!e)0$7*~COQMlNfZMa%I##KIZpjvA13tusF?MeLtIqyFqf*dJn7aQsTaPJ zj@gb!AO~6UV6o$l)%Uo!rWLH)WG7`g-4TroJ>}(5V7h{rM&JVG)}u*y#x0($Czhef zb$UfS2ctIAbwm6&_>BOAqH$&;fY-79c3aqq2mQJH;_>*fUSb6)YPhL=j=hqEfDd3X z@9Abp`XLl=FpVy5f^QYF_LY*YD)Nu~d7mxrIKrVKaJMpS&>Z|^NJVG@X?{q*4YE4# z$n^8+<`OdR=vY?S1q_GIb*2`z(WW`P{`zkd>CU(g!ncyzfEx>@`>hPR6<|~+hYLXk zR&J-(@}HPx&|AD|`&r*OBCF7#2**Nwq0-(tMP@>VRFG)j8>cc=jR$Yw&MDc}Hx}fg z1=&!VXPC=&9bS16Y{RIho|jY}=o%}cM+jQjqFR*; zSKShQ#Lgk#>pGuaOe!se4?)9i?GYmU&yL3sQXD7Q`a3Jx@+^6oU`Te)4#1ZjZd#SN zBcP}K*ID)b{>;hBbJ&B;{5J040pDuI7l#7;;9gpGblPK4=KmW9^S_P+aE*ukDGmHo zwa&4Er&rG3238VnQXfyHMc)6n>zm$gLJ1u!>}qG*P*FxlIbNo;*dCG}O*hOcBI(_=mua$h^RfTxaE%oRV_>S*sAM@j@eGEl*^dvYTpb z;FAVN*orXKgwKMDANr9~+(@0+%jXicK$F#ICcD9?HOiWM* zyDn1?tTie;pf~-H5GmQ?x=|b*_?TXtXt5j)%e7`Pu;rC0lu0o-WwdGvv>w*f?EDoa zge@ru|82edQP|Z5dCSe3)EY6fWd-YKbF(0oEs0wi5N)y>LYd%Pu(+siPF6-Wv32^C z&sh{Iuw`g&`Xxpq0jVdw5|9D2>FW`ncKnRb{UOl&_(qLw-|qkqye6rRQ#37dwkA#_ zB$2G%Fm!PIrbu(*nUw?Ys$k6WzSXb`4o(=CzJQqi#tUdi-rqX`Z%brppEoUtF@`L! zh=bbVEKNX_(Jj00Ds)nYw%=F3s-gz?UMT}=6{hy%{WSGq2=kiW74MpB2KFDEt~C|; zfE7$ja5$OqjhVaf6@9sxnmOlam-G)~3nA8Y1Fuu*41Iep7k^#PSIq&I(9Y1-D5?H& zCbjYF)j;rgX|%+m4ss3T=z8y~<&~t(tOQ(Ih{a~CWL6^KY`T7@TH0hxb$+(+kROm< z*S5PMVEV&GqSC(VK#ejk=Q=eu6OaD3g{0$j@yy=gFz2PXMByQgdC}ye<$~^A;RPps zLXg`r549BCyghUTEK76n?;tcyUz3L_pN40ko$@}V&W7%rcRaqX$wwt*2bByL#%7$! zAG0uDgM(VHqzm2IzWr=|fbK#B_hxvZGHj^2bqx7-I+yqe2vl1f5;|(B-8Wl(U-rJh zspj(!o|0$gDo>EJ#H4s3sRBRz@bvH6*JkTzCIRz5H`XxF6nlq$V0Dl`tXD3e`Ebr)J-Y}Ou2S8b>dI_t zhBQ_O+XYaLB7!cJ9iyu_rG-uYeJ7g&FhBD)Yn@f;;W33p=z2OF8GWala@24(zePOn zRr8=j8;r1P_z-4Rnv$N!b$~C{j}}MXBSw4k78Obs(Uto^YBDWrrMF2uQA~#II>Nt^z9lSe*WWwqd^19N)?hn; zbmG(116TGE2jj?LBbMjRuP5`YAxm1jYBaC@(K=YwO}_<;iGa0Y zQl{rKg%>>H`)b6rG+|Xk!2>$BFE~U-r>X?v)#T)Qr~$oIGxNGd#k{5EnfA*HbDAQK>-}7S?YO#T#19Fe}V`F1k`L< zwJF;2su)xKo)u<9{Qbj?4*LTLzojcRj1_GGvw!lR@l0q zKbP$>v!?&{L&@O1AnVcp@R0wl&3-UdsOyVU`7T^~jrqr-e&~M%>_<}F9&g+nT9hLZ zMN4X(^eFEz$pG=aP1Lv%2tkRc+3i`G#CJZf^Iy9@zu|lR=o@g-d{*c3=H1(?FaR{B%x z3+>Oq;oQC-*I+XKDt-m%tMz-^kY>sIBj20Ee{TzPCunWit-Gon0=(RvdbDl1NU&R+ z^*_xEVni9nH{QAdHnx8a{r~L#!1pP^_qjoUR6X~JfCNyaWnw?`L@@FE!SC_{;36rb znv*G*Xu9T53n2t4B87?0KZ}v}9+9#g{#-n2x@L3i!%7qf<*jmU#zMg^` z)0EmqM31~FhXKB23nwkh<2`AqQPLGOMo^kffwOg`tt;99l0V3qjD9_NcDrWv>Mn?i z8`#x|D!A!I@m|ykLFHZ1WD!-Wa=Zuy@?>j=c2jorIN>nHR&D)thsuo9Z1~`|6vdN9Ij_EIpB~0dS8TgjmM4Nbb2a8W z>#NfqEV04eOS^+*WxCpH(Fux;vG(noca!gk)*OC<{+ye6y0^slj+TzkJg{Uq)OD?( z(V$w&_`dc0#P!{4gq)$uhRTIQqck0cmtSl+F@3tWARUn?opo}fJ%{Ip9Q6H4+q`~t zp2EjDL(>=mL8uc|Cd7=pqXAEpOq|Yz!=5*W_ml@3J`cP8Uf(zDCX|tDeq*4(w4cJl z5J;h+DV_ua8^ovT@qV7E9l@JG_ry{}>Q{y;tS&*y7rJL!4{=!Y=+{bQBJi>}WHc z(e$8Do}%ZbJon&c(pDC6DO@xMG8`B@6(12{0mc=Y)>y$F#HKmwIk(S@tu4QctbFTf zqHj>RV_a4lHi{q7_MxF=+Cm7jcdifsk}H5=-6TY z>#p@ZMddSgGz{NUn0~B}+{ce4Io?cKN_?Gl!&`Oh=j=N+n^iWHQ0G1-?@;FNW{?g< zwOsP&guZ5v>a`3nLe0}#)8AB#-b^;MM|6je{PL4}Q9+9BeUXiQ94*Zg5}CePT#=3@ z6k>4%li6sC<^CKk2_?u~MC`{fWx?F=)EnUFfi8|9Yb498Wr12 z&u_1Fw*AQ9$y2$Wzt)Ix_Wj$!1mnHP7cnX>-tO|RW=kB?eYxSqaZ8eBN$>GLjC-YM z-YL|#8GsDM5^3T}c_UN3jo8VI5#D6DFUOJP*8Wwi%OH1CWpe~pg>Oca$lKG_!6drq zNCZL(!~eJ*%$D|a0Z_KU`g8ori-pgafVxaCa_|87DZ%!9R{w=HwvUTnh@7~2t-Pqf$}3X~WJyylsPd1sEFRtKp;tr} zTCP6~!H1a*a@xCVJ?R|~D9ylK?9jw3@yWd&hBhGBi$u4#H@?>#yN;wq6gX}%`~Buy z%QF}p+#d4bhD~D&ako3&lRQ}7Kp9>15BG%Ny#sd$AHpNO1|BmYohP|NbxGd%z{DF1 z()%9eK1q%XXue$HJkKRJs*0SS`iDSM*x7K5r)iD3@6Eg{5y#|V4s%U(Uxhc zB)6i!rGIeMJ<~UD?0o;3J)41!N;+WaD+!fd0L%f zZ=yy*0XW#sI48$hAI~zKsEGh7CezER(2T%$Nivc zM6?i$WKjXfPhWzIS=884H~nF|w^sdT%fwsBi%^#q;bCt3Ic|@~4~f5p`7Z1im-U8A zMx`VdaweQPnTm0>o@SZmVj1SRxY^p11%c3|{Le$l+0a0~Jc2WKBvw-q8Z@=1&LMo( zn98y%_6f&EL=|&w2=4>+`Q-ANP3A6KbMc4v*-OSpG;ntD6ZZN3`!jfv% z%opP!PxC^D_c?V8yCC@Gahnu(LnZfy!FMhGa5y|sqpvs}TQ6Qsziw6050K4+U+|0O z(CS+tIpS_<qs>4e7`<67`*o4DOtiyAwRLG?Hr_8R8Y63b;eG>KN12kr8FdSNdEZ(TilVB6 zKLc@Fk1tN^ZSpXig?n(U%LjXND%T~+phrDA3ceT4w7ln919P|BfEoII!i?NlFSE7* z*l)#~cjNPJXTmUnQdEWQqALEquNM9D_!uhV&gXChxyx+ac54_q)d^#$F7XE)Bh-CC zMfs`Q1U?MYN&ox}q4jnX*CAL)xc!H~ivBydR8&@|uNcnh3@$M|to`nry_+8A`}?*O zmC@Z|;qGICwb=c%a*ywRvfi`YYX={hp$S}w_;gm^)%!pcT>w}XTs^A^gtb9Hd^-S1 z!GwKW+xZFBwL z^{P*SThjTX|DQiP{v8DOr(Wj2t@^}DY9GGBQs|F9+F@{dgyDjGhiw~JfDNo_I5?rx zDQB7L(V;5puy<*zao-TXzb-!^ZM;!5c9X-kL^5+^q|+6z{2?Gj`WpZ7A6Fs3vcv1H zsMpz`hFSWgM-J+LbJg;jtPrr!0d&$=RnA)o90QW@KiXLX_-grm3HW#j;!(&3;ZcXsYo(=%oKv_K+_-LdDY~~fiHjNt4~WA0hH(rE zs`wccZB{Ov@#3?wqw zdLb&x98uqwMJeWneYR0+iVSwDD$36~gBWPZyqmS72jBrLKU)tfFPbfqhNHXcD!|^m z%9Hu^1(in3D8kZ0f4l|b8s7{CDhB1wJ)ZYWPXGCd9o-OM09)Ccp_=d3Go2Vj&3CzyX6asT3DQ6}6}j$sKnGmWLLU8_7C_{eVlG z@2+uq``Tm29HcFMF%%CQY5suY{SJi1A?5MDu=$fFMK!iAV*1e~76sO}!rP+^e!ID9 z<>Px&Zia|!yaVyTvb5$X|Dxq+q2tgIYXdNpToL@LO+vYF$7mr0vcN4)3LGS39fzX3 zz}n%rFXW)EctG)Uk7Xi%8j^v?M-;s8*OGZ2m{7lev_rVCwdni;BLMj(^mgqT#evOp zIop{c6*Xiqbk(|nximiB4}q6Y$T%lCggQSr(pz6@yf`=n+g+h-GJW+nN;bYG5_*+| zqGWop3!MJdAUU{Y6MKftRA^l0$&u?le7Uc<2W70CayTb|8q>{Mma17fOGMe zmVzc#?uQ0ngFZyEU*Csv*01(!v-|qBWE!jGE3G;62 zBuFxfPH}q|gJHOF_otIrofsw$0>- z3-uXMp7ephB_l^fi5tVwy1JYtoknIVwOR}XCQJ0h$wl)ksGiW3rYTAn+m?O2Jf9|^ zYHjvK@6jwjr2#*A2LK4x7YL&p+xU95itQha z_6mdA${~lR>u2$E>(?u%!K+={)SIXZ%4}v39$pU%8>E2=ECk;guT@@;dmiC1$iPEG zG%1pOu>tw?irhW4Um0Ih-!iY$@%_r?Zb&6sMA7_IbwzqK)PFV&TeLgmrC##X9ysfd zT)kDYOJJh<*$@xv5~(92eNIwQJ$ajd?nYnuHK?SVCA1=K(*2e^L5-zR&(x_Cu1qdt!VQ>fd5iZGAX~T*&SvaqSU^&Vi2V;AMKOsWiweaM(f=R zkImCulyLTdoq(24c!XVxaEwZ?a9nwpcy15Ebgc{Be6yr!#5$xG>fc zNT)^XT2tZR%a24a3Rlb4o~q7)&W_UrWBm%v@|@(46}2KGC2{OCwHFsE49kGH-R=CuYqHg4U4dtEYVMgVud+0s*e+J;s8`!88m_F9h7r|n zl}{GZdNe)eUb{&J2PjSV-?oo@i57e~$-VH2$CtIKY)h4CHP2)dpEM{AyeST?EU)@4 zbbHO4WpS;gm4HHhZ%H}ss`6{_R zICLwmC&blic&jl?2&#*#t5sA1<(q1}dmz+$UEum{4TNoB2)wDhoGZLEHZ7ckinz>O zOwvBcyRyTxv5Ao)jL%0CnoHcxU)G%ys_y7jj^Mu%T0zP`MmH$ z6Bcp?j$J5PaiS7v1fv{FN%hKf;+cLnm0!|oJoPea@G zUliS0{)CS>^Lq1y`e3lOCTr1o)VT(CcImO0UIYemg^-?oc=oGfd=jbp;w8*Y`Odl* zgBWv4)`P{*hDwX8%bq$NXEF#-j5@JI&RJ1%A5WHGM>3hRe zY>mrv6HPp5!m*EjXm~r_x4w>Ww`_^SkPPJMl~aXBd|zGU-My)~$G3*f^Vu|zO=VUg zR4c>6Y=Z*a}~9)*{I~4D9O_Kl!pii zCxd4qB5&<=4_ggsFn-hbHBpaurk$^nx|vE1C(@yr%EpBuNYXSq%Z4iQhrmqQHY6>B z4k+5jN23mpwZErVZf~Q9Di01(EP68lFef-~)D`5-Fn%*GWAxgss?<|E%VZ+=Ov7LS z&RMZ0ff{vfc5v|YFuOczEp8fgFX1N1>4eX7r}=&9e(d7s{=y}rUkaaLQ(!%5p*-lZ zTZ$FdIO7J3&qC9yhB}ZM7;Qd{9gY!AV<)7Hn!5t!8`iPQq@a&!uD;rUgw=j+h7jai1&2RBT*2bA4 z>Cw2bH3vjC%QFq9_;MN;zV=(3@qyWNJK-(ms5PpYqV_gU5BY$PYus5gCb&_|@p9W7wkB$KlY93vgKQe{&{R!aXS zaN)T_+ntM20bGl#0Da2hxPZVZ$icIkfN%^1D98c`Po?Ui_4YTBwBwb+lVJbk(LQTD zU8D^(ovvB9D0Zq(!FIwH6(Zsas=>lM4BokW*F`;>`^`L3#%BUqJLb$E$N&;y1Y)Xu zhR$cgy}4xR1a=UHRwob0H$)V=m7)2Zl%>aQB|wizM5_go&)g&D8Abj?sr z{u=?m8$$>;r?zEc04-$56VP-lLh%>$_sR6J{L(5a!k!g^rw_1P%_!lUYszDgWsteC zKX)cegZQa^Ix-DiUDFr%1-o_UJqMrZU`N%}LHjfG;K$_Se=lg){K7LfH|WuWE5i-6 zR^!yzP zgDW>2s~k;Osgs2!lRplf^zZ90=kf$BM$9y-RmG6X*K?|5+}pxf1Kcq9VZ* z`AU?n;f@ZmZql=?-=v&2{)(V*DsxTR9Ha?_4FH(2w>! zjD?AlrpL{aSau9(tzT_*G&n4ucRhG3aBsGoRgsJ{qj&|mp8b8Gu%1+Sv^=XL zwh1Qq2&L){m`_-ZjabnUY99iKLdh-_N#+-?4XFAqT3aQIx|_v&vBBmZn%moX(=u>` zIAq;^YU6d+w^{<8!O3M+izohsRy<-s?R+C-A71VDw@U z$gCuTS~^!c;(JWxq;f@)rc#c{bQnA+*j5f=9pD4HG#=Uz8TMQS&lvI}r*4tg7do?= z0Y<{7_9JH?11hWK9UFm*W;TDDnyCm$&eojDPA3wkd)6;api0o&30}6NJt2>OSI|+Y zNWH{q>ek?GW6GFm3{cl>q<>k0ecMlN($QWE$MdFUCVohxcTfDp~m2nvk^&UD- z_WzbxC}y}ZouNv2JrFc@YkV6?57TX8g}MgzgxD^n32-5J5WvNE<3A#_>enKvuP z)6AYcwYC9F^KKk#Ptba{L$^>6DzRU3JkL$mav$ii4)Ee|wtc ze^mAq-!&1KN^PsElH;WejL;(|^ooz+Swz_f*Kcvu z0sY!cyOT>YPNf^#BVkc>4)4!XuPu;e#+}XUA-3GGal*I3sPS&;*5mqe^&)W1Mca7& zfDz)wGGSVC;-N)zn?nI~>WEFsfH@k4@KME`fb~sweRA4pKf5_KeY6!7fOYM#@7(&N zB=CKkTf#U6;X+63s#x%W#wA9dm%;q5dT2-DUA1I$1Pz2Cf2pOH#z{rR7cdzi#wIu`vFbcfi>8 zWmCiYtk!+s>{;m|x!__jG=Pl$MdFIKG+*WOxGSA*O*0RI((YJWThm3Z)VAHe*Sr|! zM0hK>Sb2ucQCR`Gg+a-5siGp<-)9^bMQGuS!fKm{tahZ-0erdQe&$5lya|P5cXTZIaE3)5ZkoBsnI7gD{*DZCS z?;W;7RXud38VJTa+V}e~$7>m8i_vF8Ep;J!`dN2vZZX>Qn&vtm8`cFc%~yOrRb?lX z2^nD2Tayy7o$jXIpV|(lYPK{aNs0jZpcP&}%hEa^0@QM8;wMwWRNJG!mG-OP#%(Hw z9~Cy)3^-j;PaB5YuTGQe?>3iXJK;HH*8n3<@X5;=a&v?*Y-N}6=*C>nlPc3cPoaOg z`!~`qlK!u3k^g%AczH?CQAazP$>ks)Ft0VkUHDUNXt`Ss@z%uLy`{%@`n|pSLO!QzT|_?~_sM+)tV87H z>Z?;>G3@A>u}LTgC%-Obgmd3!$ETdAD3ka%gRZ8XT9+D|54EgECiTT4A*Qma2UA@) zN_a@u85Y(}KvBGCr7$bs6M1a8_0_*@DlY4FeVhno}c> zGCWf3q_o;(=d>qSKib2rjL%bbyi57C^&dJ zF@u`o(m%uv66}mJZms9>OrhLI($HjV{WBWisa%#0qv?w{N1t$Msh$>$lB(NHlrH$- zfG}UMJt8sE7d<1LDPGM=EAAubR$NRO=lF_8hE6MNU=D3l3%s~LTGdt9t}yzWt1^8X9lNq zQn{J-6JEBcXbCgQD1mL+(yGx(JN$){X20wdN?a>!aQX6o7)0_NcJ}N2p9+96>KCQk zPfy9-Klpvb6WCGzW&8B43oll6`0VPlE+dJaSW|NF^d&x z(5wr6ICbns)Q6}T<9HmIZ60zYz!#IJ0&dNkiw)##I7U{BXM9<~Iag|pA2C4oyL$%V zdiGMJQWG z(sXsoU$q$;N7v;+ltW%S7@DU!XCSn`k*%|m@n|xOcMoY|otrm$t71}O);Zo+Os$hV zy#nzq<2)dHaY`USk*N(`1@$Nyv0;7=H7I}m<|_`UHXL3Wwx5?tsF=vjC|lGjBAu->8@&NoiK@KK?CLV%%;laz+Rd(>$AG=GThn(6~`U@T0iO5 zxRhYn5ja?<{#q5oJamae-@o2gqTFT_Yi}STFX=~KgqWRn@CPQ|+mMc%a?f7sMx7h< zjO3mDz{%0)WC8?OIyxqDm@vE6LTlF|mxdfHBY$wXZy3J{QH0M9qLGy2wihN)Q^l7S9edoA^WvI zfB9X7X|l7ZZCO$;%SAFP*O1FTPR@{7{{BxI_Ew-nu%5_lmp$7CKHjVi%rY z#qQRf%LrcA48*$>(}R#4nyCZ$a)y@~m7GD#)&fT5s>$u-8Xs}~%ASaa*$4suM+DyASXQsw`EtK_aS25sHM z(KHNAM|@74$P?Z#@k~lY4={&&>oeKnP6l2oK9|$U12^1FTU3q}>%Coh&d5G;%QokD z`9g<%=8fgzuL0`$Vew-24R!s_2AwAcj`CZzXk}vTx-^J#{^9hkV^KYNW7<%l-{A%CjENXiC_Gy1;u8Gp`2Ok{(r)Vt63Z4R(CIa_? zgtqn_fPakoj(eN_<%g%X0{^lB_TzvjF#opiFAHtsXqse@-19b7t{t;?)7V=+#d5|- zcdvC_CL?OrxX}Fl&Z~htyXUkG|Cc*t9CV&;POqMQ8&3HAY?pQJ`fQsmbw2XLr?A`O z*ExxGfA-?GSRQpUT^gO3v*GrdmmGEyZoF3an0_uS@y}j^FvgF&5|8|^`u_iQ7sHV< z4&NQwQL!xww6|6NeH3z*;+V0R)`A@oC?@3~8RDjOSG=NH!Cbu8CJov^CFon*lY>D; z#mfbm=beANZq=t0B)O^R@_quE5PT8T>FTaI-W}}Nj8hIy|M)Od`s9u1fy@>I$x|#( z=~$sh!6AfZdx+o6M44(Gc2_2qeyt7hvXTua<&KFf=p1TyC-C*p-{EoI2kr*L2Pyjf z3()*sxR};Oy&my*`82kQG=JUs?NH*cujkP;X1! zd___OU>u+B&|NorHtSpN^x@Gk*sF@6V25PL{Py#_QGISy9m6JY;L_(Z(y(Ru%MmB_ zwXxLkp!*zwKlO}%{UZKz77K4*VE~VXKarIdhi%Ub{3sKA^}y914p<0SoPYYGA%T0m z;S8ejzSu8wuMS)#!Ws|K9tjBjnso-Nv*`RnI1CehVw4T*To_o~EdH2Gg}OwE9Rpo! zS?~0L!vIiCTtw#2Rq+G)IhYI&2K-y?!we^FQy$0we6Voclfx@TSkU-DgfLxURx|zM>>kNs zd!28%^4O)5ni`TcsQ8|@;_ zo9}7&u#lRv(bhagy5)ZT6Oi0;hxzXf71Xd^BaK_!pc1B41({sLw0kG|?q%8=JKXe= z`TMtk-MgREmk_BiH*-iqA$IZS!TO^d$NJdvY0GxGF4<4XhS5HWaJt1}Q%nnSrF?gd zqHhH^IBSI@X46^`AfBNLi17K1XW}oNbUg@2&$F!SPQ7$y0~=;jM|FcwiPlCBOs&sw z{mI!st+!po<%tetvbroL252A?s|V2v)v`n4Ed|wtwp%L(_v=YF9tVwuHpGq$kT1Fw zj4h_5mN&9!IFQ-tc;UX-xY%J5%w6llgxudZ!}$i76fd1E;X68y;V449`LKjroJ;Y9 zLvK?z;SGr}nJn1p(-1-B_LiWwvn_OnglR$PLABKkFy74r$(%UUQuHZmc{?qRl9K2= z(vcg;(9YXHL*%+VRS?xc%I0T70}rv3211lu9;T0y#fm_;wmWtRMGpirz4gqn<{*ZO zct@B$PRc+0Tij4%)yqOn=9Ro;`~rDa)Wb7o#aV>nc87bZPT)7u?Gleg@q=^*=?R^Fwm zNI0E@3Id?D^`VRf)9|Fivj&3LppoC1u|+n7_$q53hi*Tne2ek1v`o=P#tE~m>>BER zP+1*Ad68mo*kWp_U`jy5h$&=M3hFg=Op?->1QwW?Ve3GXuvAf2n$%hB{2bTx*(Z~u zMrD~Svca0lAUHgaL%B0J#hogp)H%bDKWW=;sFj?~n%olS(QUXH3< zgjxPr`~d0U2diSL=ur5u?{9CzPq6Pk1+(#$6@kh;#5WGeb|ix*x>hl9@I`|vazO$O z1at zSBuv*I|Y<~za-g1#4G)5X|~sVWywyp7^l>C$qA5)j+N+S-F^A;s`|b{P6gu%z}-7Y zX0lt%vFkmJg9&Bsj;f%tV^eh=*#z3c7IxG(NcP22hxV9R@r6YM_LEh&ol2;YNd0CZ zHgJW9BOJ4d8JuhuITIBhe8u<2u+sQv)2(a1K_69^K&>M0J-Y()u;i>U3&q5RT*{L2 z1N0Ol;xWE1XM{3PeH=cZ5e4gqyij_BsE*QYmK7Si07$C3tAPA$vsMv|B1))C5Ew}<3VhL-Fnn&;ZV+533FhEN63$h8JHD$*!@-h>Z(l6I6lz_hVDod*NW%%K@(pkZh9|IdG zyq0<}8n)Y2)a-FLAXT-DsF@tr>GD;oGgghvT3Sq5i^)7xJ5mVgj1k(H8W~2M@SJ$J zeNmRj87Wk`%wb7z!6{xMu4S_doySV7LHReYTa`w4>DuJy@x+cNy`d^pil%L8c4a)w zJ=6K9Ldd`E;P^@5d9RapRq-sNwD&}Ck)i6h+3vrMYX@_tHdK_iKW$e}|2FcrM@9Ng z)#M$k{+xpP0w)xQ1`TqT?4`GsFX|b(@?}eRc8Q-;D#b#EtZjIf$PYFcEV##=uNOjN zKg#yZ0`7G?7WY-oj%|LWE>uY3`8b-#Z}o?NBfh z+ftODefS}uRTYz^sX!W3KOFk)MBX4~`gM4_fLY}1b2<0Yl>RiRp`-6pu|sZh3Jg=a zXlYuQKO#7BKPDJa70-sHhTEpI3d6JBbBd0+K_=c+%x6PnPR2&?j zE`CujV;uG71G?D=Mc{(@R`7sqpJl_b;3Nq%Khz^KRgTQ*xbwi_ry{zD?(23?yEZjT zS>@jBljAYAq<1LBiP=>z13doEcO#-iz2uLK^T*1yRCM4$rchP68C-C(2_5(j*yUI^SELc-C$Tcb^S=8kuv93}b0uj)ndc{5XKTqjJA z@KNe{b#a>{(piYkC6bflEfsY@w?zP&zB_i%uhU_w_9&T5DDUfR>O9apJ2DHgl~bC` z=QM8}QD7)kRAdX6wfq(cb%z{%8+TDCBY4}t;#G!cf?zK7g39v;nHB)+0GZM`92(ZV zqOPLh@}g5+g?OjN-J|pZGC@5qIhN!P(JpHd^G8l!B6aJQmcD&rrf>TTQU{JCS>|)P zC70FDcf77_22w@$cLbhXeQ;IaDiClJ5b#ERCro|Mro8zdfR^t&{@G?DAn<=DRGfMN zWM}^Q_PvA)!^H#jtKKwe!-60~9aKJRaOYL{1)5tz48pBwJPD zc<*A-uNQJe#EaxU968Qv2^;B;i*Ma^~if&>Rn_Q4f9zPR0)%SNOO$mPVJv1>i|enjs$gH!Fwy>yQIrP1{XH?eJ5vg3E(xly_*|GsHObUoU1 z%uLN5;&0J{8IQK-*fOli*mme~B*MMeGUSKO!9|5yU379feLu+bB>Qy$gy0krp_{g= zqyn0FzogvemH1ptwftpO6|atGY~1aHocEuQYaWz3KPOSu(hZ_15gg{eSsVgCWm&=j zdoyEHT*tlVh0K~HfF4=LB__X7-x!V|;fsyN%Z%7Sq1{C&*~V1{uu$Ef8KlC?wY^@$ zje3%hZS&KtGJx(NB_bKPz-xDu`3 zzq8y8qRL6y%yWRhczps7I5l>DpypiTyz9bn^`$=UU zCJ)#DAs+K|IH4m_b{s9{8`4gm9~dj2@25fo`^hsoh$3IPGSR-9*loE*0}-q>GndIB z_G3sWhi0eeSW~EqI;5pYbMNZfmNYc2yx0p$Q#7hVN6ch(pm4<8k>Pia&RH$Q&uno{ z|3#dnO1xJ<){umUvS~h0s~Eo+yonvm!~{-DCdU%U`;Js-*`|&idc{iq9Qd^k=|4kBtcq$&;o*h5JC&RsfZMX z0162x1du?0P^E;91%%L%s)V9+K{^T|ivNwCbFZFz-h03Myzlpa?)yDwo+mrAXRkeb z_Utul*37I~zj(-aR#`0XnZ;}QrMa^j96j0C({Mtep<-@UpF0?WPRdsd_U9{To-+#S zXpr(RQ7ta7eeirX51Qp&&uz?O7|z+>BzG)#B2B(bC{cAf9T`oqG$`~Dcbhe=R%X_c zD5p}FQR@v8f!4*EmmGbVv4oP?mR0KsJ1soAOizr(3!$Wdf9lS$%!%sO^MeiMa49er ziCrr|R+h9MK?J`;Ph!jF$74q?c`@mU1z4Tk>40??v-n%DmG+4X4C8R+S)VGwhORsQ zQr6AAwYQ5=8kwO(v7@ppwTb6?-hF)mPt{^WT?n27rxZL_Y|8qiC2EUS;treBv4D9M zTc@qziL6=it`JQRsH!!Kdt7mCe{W@`1|zFeUk>dWmOmb_KfHuG-$&9j&J-~YNKQJI z^S33I~n~a`ucG%2Ak=?YHJg2}{wR33OEUz=~Ek%ftsA zagU~`o`l9I1BUOczG>DfR|*QDhf@Ph99U-|YH@IXc})wT@5Ck>>PS>RKh-)wf;f9& z^Nfp;xI^;5jBNUGg-Y^SxlMuUw35}<)9jG zWOMp)`O^^e>@)Fz_n%px_R@D{oz%nI=XYgg9!uj9!^cz9DUxatGqrGh!8cHT)llvr zGS?W{u6lI5Gsqu3;oetTPLxxIm!A}DZBEu@CqY@V$3-+7Yah28w)M=zAdT0uzXrsk zbpgPySNpccv32<{Wun9+4lIqCu1jn%>o8SFid79brrndHmlAf8ZGYk`yY4gAsWC)R zE#pEb4a2EPU7_2$YSwNh{C@%pM$1b6#(_a_J*H_G)Ijx|yp$ zqDK$aBMP}tP%ybZbJbW>Yr7UDYu7mx zxdW^PWwB;*D3Wwk!~n*vYjJ$9ZV<$f`^0<~~af7=?0vZoMK5YTp(++o7 zcc%!a^(S3be&Irq0kp2$*WXekv$DyIWjLn>PUFgwD~=L%=ldnWl@mU(Du zEFvrY+UXbX-AH+QX)pZTZ^tUiuyI4vnwNNZ*}D||9@(X2o0257Ch)WS6F1^1>{!zS zHqku@#!%2bW{AiV^ z*bP`TMyz@%Lzt^PP3F;;5jpDE_3IIXWLQmKcEwyDmax8$d2+lnn`%Z0RKPJ>jxN3M zd)W1=n6ta~Ry^R5Ovmj=V(;$K)d&~mY-XDwd8plZBC#!CqU8CiJ6}WXXR^e+Dtrvm zpQ9h$Q8!ao)_%mQIPPg2G`t>s1nXoJE+;8*-j*wao}?r~6pDA}wN|>B0=CtIj_OfIn&|0T1Jjl zTx!L!C4q@#Gv~yQ&P$nkJ_uijuh;q-k<(cdp9HF&7Bv@}`iTXY4Egq?U^6>AbK4*{ zbcGk6*TeAOK977h5p`q&w`~6@<`G}J4I}es*Q%~_*Wlkn?@+$K5GSTbth|G3A z&xv&O+>uaOq*hrT>A4AdXjms!rPNXLPW;v3E<@T!(R)CeB0V{p5Lj#*AOv_2 zKy%xjV*R_p&fiqbbAPJT{hau}UBx^HU~%pR{9)V@*vohi#6k}eD1EFAqDOa^#~){1 zq4hv)#i#R`wVKQ$dPM7p*nQ9T?K#<+^ICh4_U_yu%VEHEUUJ88*JjdH*u74CMQvC_tZp5#I(5wXC4n$6kIi~irO;Hzk2H?4Fd*s99EBPNmp*J9 zeAia^LXaCbr)7fdGsBmNx?YNk^@xY8E^gtDy=yxYYtx?;7$9viHawcWnAeU3LY zx~$2s$7C0y;Dx#!&H-cx`FEl&&Hf#HXS!6c1geQkRxAsps^eeMtJZ5|Z5@3S=kusB9ka<$%hRrt zc#rs-s5HxE8E5(4)TX7idfoavP#z|(_3TML{X`DCt+!6geSl}GNf-+(EIvLyn0r~& z9Sxm1=esAxCpAXJtA4adSakPIbtjv4$Bd@WTBIp7AC z9kDWpidLjVBL&kA-QHN!TEv(z%hd)B_P?uaFn(9#%z^rOQ(ktVFcgh1}owB{IP< z)%Sx-g`bP%7y)0Ab)+yX004tZI+k5O;`J<9ZJ$__iR*yB&gbk3Y%(hHCQIH7Ca*7t zD@XaB4}X#iT=7d!^X=k*3^-9&uSh79u_UF_e_BBjmFy~)_VHOGk27}KIqzd^iG{~d zNA@&*b9O&A&7!$GsGFlhtk%m5N);VUu+(P|7Fw8c zaxa!Y>0YnTLV^*DVpLaR`KeavGv2N_49K8%D6?{!`^O+9cNGU(a^YQcRc$|p z-}dJTY;x*<^sez{-I$Rp03_!!fqbHfisx_Mmb3HGAOhR$HNkh4Y|#k5<9z1bw%U65 zLPu_yV*19Ee1_>F^(1yL7Z)N^zSAJdQo6FMYq0G$6`?xf2$)Cu3K(MjWGmjEZ+GZ{ zbhgUIU#|{UIbfPc)G==!c4$K*A?=)PGbP_Z=lU&D%}k8E$9(PIev)hQk{-3{selZU zIEDH>-w18-rf}v5e9SAw&&vsx)g+|Z4|*+GrB0g06aBbfhGDS?-%px4UzA)Gwr(H- zE?7m$AqPTn07fvMr6DqtO)Ob156U`>C1BH&dmG!j0@N=Ll1YkYPy@2JeP;Q_G)MXN zS~|z#^PC(FB2%5wXtNKI3_GMW1gS4@jASoaWGbqMejqKn_J{d;q}eB)X#w|-9-EIJ zom4R&zn$v!)#t_B0M0?*6{6!KLJV(5O74tAk?|iMY9!9X8guSkPwKRl+>EtKzh~vH zlJ5hP9SyYf^vC!Pnm<_oP%&=NOKeQ7XG9j}73 zg`0A!YUuO4Ab!O)IjA3d@YeOU7Lgb+-~fZQS#s()I5IJKnl;jGdEXhjTqwFM+8iXQ z0#Q|WY6*Ezz~7Q5Sa%}2X}a_sWnY-RCbT7*|4xmV+1K>9C82@e44Ci|jp>FHAq>E58fWX47Yg_OrUWX|Z44@SS-#;8= zpwHPh#ocwLC7c&hK6c#Eh8=K*_Rj-=`2+&d;ng>uPVQ(|YyZGmMKk{2@Dm0)ko%`) z0o-VP-@{({V@o?5_}_BOnY9l90H*^$6InpgkG`9J1MdC>XikLev#)x>b)kO@{rYY? zzf9Q!0+Z(K3zww+Kyw4VKnxZvo%J^vqdRw@wfe65dv^(+J0Q z(cWWec*X*v2Mp5B(?V8I&gc)Y4B$Uk?_}lN>n{a44c$>3n_L%1rTJpqi z%Q+>yUeORYX7?!TD4~6H*GBrVKp3E!0ju>i%)*$0$-5_*^UnIP0MOz6Iw^M)!joEF zl~tQEnp;Y!Gd)qmq(pfY45%*j?DXkl>a_~F50*Q-^kapg1z3Tqalar&Wg)Q7B&^;7 zqT1Jd#gV0q1ZIV=L5ZdfU@;#3*QiTwP43|OT;qV;-ffxdPoBlzDk>r@O<&+_*tUw< zWKX>WiybJ>6bHJdw1bu&##aGl95dYsYxKNcJ)3TGEE7upV(T&2AnD9>ronYb%fc`p ziorRjOk*S-Oz6*eiuDZ@N9A(`dv4%^o_?h64`eY4#RkY>A%)lGwNz>n=ab`HEQYSN z{3#3mNZf^s1Om-ZCdk)+Kv)6DNc>tpbGyA)vlgnN3PI2E)py}a@!PiQ&uN?B%* z*p1sdEt`w&Z^oY*YZqr<0Hy}*ow%#RM7tmFvFH4e0CNF)Z2ZSkRz#Y^lAu6} z|AaQ6yK938r1Dj%)J7cJQM+!`nRhRQu1%Xzd*rWO?0H0?og zGMX+oSp~fAmzT~q(Rf^vd}a*r)`56VgAf(%llR(X3cy@&bDWJK&fy`P8>Y#1A8YS? zx`iT`Zi^#eVzh)xKdmjDe8?@*kk?kakpH&ozKK(&d2j#+!t`WF-p++viky3B6ge65 zf<3m9v~7LE9v`v5PQni!HIJStwZCYiK6T{u#QJOdi<-9V=UJi&Bo0kzsbmzPrC{Lx z)gv~YP$8wolk8yn(vvKJ>Fp7b@4|6+Il%Shc5agBg(U)F#Qt;-k=qj3vzj%vKGqRJ zkcSTrvJak)w}9!&CI|O{82fSD6M=aTNig@ zq<)9zIa`_#S_E&9B-p|3gV z@^1mhJ<@s%&lYGxjr}Vw%6xia6~1y^C)o9fkbR2-l!A2cA)T~%Ke zkZ{^X97IxEA@-R#V|F_Kxm{-3?copX0r^IN*zDOI%#39UImGa5doU5^gi`;DNI1sx zEe3x@F6_cZIIsMAO9Uq7@Jr-fg0#I;{&^p*JI{lUgq&XUw|`q^j?ji|+&2VRVc(I^ zD(X$ZCy30VgAKO!4yPbL55@w9bQpJ7m%MMFUCMb%i2}Ndijs(>ET6PX*nG(U_=qSw zJr?eCSLgVH=x%=KUBSb5>Ff*#8YFbHxTn@g4@{ya-q(~WocffZFAK^ncJ^*P1d-Q4E0cnetjZnpDLyJ+WEYHj-tox!O zs8gj!-IJfOoO1|XHek1xfmR8v(Xu559SOF(Ph$+(2)%xH$hk^vBmJP&^eTB?Gya(z;%Mk= zVbGvbZ`cT8B5PuucSGir#I5JPVFT#$Dwf1Pia1LxOgyo7$@Xxf<<{qpyR?t1Sj-Z` z?*v!zn@S2^EWSs3p9KOsK_f`J%YHx*0D;(eynh!^asV3rUUM@W8pb2~U{M+QQzK>o z&7tItkgZ~;y~m(0b0i)*)HyQPw;)F?yNK|KL~e8Ur+2uBUJ!w`Pv1b{4@LW9E{^cQ z#@p!#8Ae|8vZX1mQ?Dz+G9X$y%nDRG0}xvR;G#rutyR1Kc5-Yx>?2`-fA*4tofK~e znkXkeBX&rYBOi3q8b1Ql@0xzriBYFZ00=hDxNK^Get>I5m#A2|?1 zdAU{fL}*HYyV`QeW0@|p2o$O-%A424CsX(0q^#6*sJ#&*LQeqzmEgtD;MqmD)vtR`q}fbIv^Sx!0MoTn&*I*+wCDE zdS|J7Cul=}(;v;^WI@ph8iuMmb+Nh<5W_y>gekMV{^tB*OmXr6(*uFNo0nx*C*#s7rg!fwN6 z3#VqCk|F^u)4rWf#UMzbZ{_*UYE$f-Qd=p~%TQzNc6C+rUx z;YR66i_}YZ^klBLCa3pc%bLAzK6IMvw-e|~Mh&Vyq|A)`V0xq(NUliI&kGIl(2>}D zEPp;nteKD;Y3CldSFX^}A_W_-tLm{=8brS#e^y~hOi2;usiSD#>|vYfIV(iu%t~-2 zInp!7b#8}lC4X4L=4nZ23O9nyyZsI}I+AG)t$&G}`TC;BW#GMx9s%9e4zU}g>z?`A zxPJ3tvBiY(ydrVo?bzj2H{%IvBU1GMJJ-hc?U|UM{Frtf-YcjnV;9HaB_J)v&vS%- zoa3R;m!(NT#@g4jkyC4Q=QvIPRrFJ!!>WIt1cbWQ1>WBx8qa>Z{O3`c^=|Q}j#?Y= zuXIWGmxq&{Hr~ihd18K}D`uhaGqcpGHKZY>^$ZF^LDtOGyU(F zf_($|@Q1$#L_FuzV}TG&?GuN;8>RqiK!`T~BbIafTfXDt(%|sEA-fvk59v?C5_KbI zOsg-BdgY(w2=f(HR(lkZYf0Fa;*-gx?80}!CG z-aElUpGzBZVwXl5RMe&3{#^8lE(;xvfE?D81(A0B5rUP9!s`g*p6yEOyW)?2<6m&! z@a43ZwD$nS!vplR4hmuVHhYR7L0Vlm`dUTp;u*u6&}Pi+ob;r9D_1k&sXoF`bTS5S z8QsM_69ImPKR~mW_N@*5j~|`}=ApNz%Q}GeLad=E3nMZF=o|JhJfr!&hdaDmyq^`) zbHHhQ8&baB>42ZA#QYAs^W5=G$4?JKcM6+CYK+hxNuq)s)ZVw_PANNmt?H81cpjTQ z)w^mcpsj*vqVhU2V}N&app#m-#Nag3;?i|Xx+^MQ8}q253inM^VCVx z9=_|bPkzG8<{o$|;W>|WHi8CGvC}FLB33#)(Ga?kxR=)Uuo)X0Ee4pl-4BSckUhYN zQ}h<_ce1=Xq2qfY_R?at7=YM`?`J;_A|TL6#-^bpfMt8J`W%q9PYggwS?D)6Z}pCc zNHQD|6=|w$J&N7(1+M+374G4^`i3=q3IZ7j;}aBDwPz9`w(@&at;}{bsYDq-staU3 z%xe)k_zVpUq5=$~ZZsgSj#-!sfM~_-#P77RbhYar0-5dWByjNAXp23VE+`-bNCWNX zm+*r#Xo!cNRDA;le7S#*^0(NzUkLx~;&Yl2C z(*pz9sC$1OamAnI=Pl_H4}DhDD=enCxXT}afcU+81ERqFjC*Mna|DH|C`l~mY(fES z_%D%Dw7}E?&FV!Fz(=?qJ%g<+K(x=X^1*@#IbB7ntRKNIt^M~0@B1xE4`iR|`DTe= ze|q?9$7LXE5D1p%`2z0Mt=5o!Acr~W&Fl6MClL7eCXO71k-(jO`ca7MGjI2%gXM@? z$>@%RfGL-u27}BpxyYnvT|gcM3EnFuiM0WouJ7EkslY5_@Yn1ch?LC(pWjq5?DTCM zu2vtjCmg4vqwmGqXHDW7JG&)kT-2Q~z;#I*ZRWRtjsv1Qw_6+_{JnBO|ESNL|3UN5 zj{g~>RXXMG3eA%GjC-|x1~_C7jllQsdj4n5rvcf3#MH=TC2ZnPl>8f(#X*M$0|*|V z=gb9f@AX~}0i7&Q16uGiU?fu-Tm?*>p`uO*rSp@@Q=WT1c$>&+6npypbI0eikMkzJ z1_;g`itM1XwxF=IC$Zf3M;J{YK%m^e*Axwa&_qSF0yY@Bs{%mhl@LM)EctxD{yT#J zof^O*gzv0Fs{@vDaM(B@KtJo?Dz4tI%>!V2dmTLqJq|1l0C#PvdVx+^umr^I??kr* zOj0VVhJe)HRg}T1>e0?7!RO+YW1R{6P!90yz68YSuziAF5+j0~lrHy_rA@BFd1dU{ zYIzzT-(~24hzO}Y@{}AnsC>}Ub$W^^1caI3Y6KQfjm7dx;~^`-tFP_RMiK4PR5&Zv zft;q~9=S!6uzeXbA+ZfX3^65~Lp8j2F6!z&lq?@)7|Pyu21cDo_x$+v^4CWy=CLNm z&LZQh`rG2I)DAa1zQ1D2kr&jmb;BmIRkHUSP;x+6{~Z3Nwy@@G;)t(jR<~?#?oPt} ztMDH&CreY%m-)Q-Ovz^8!_s}X_c-gEMiv{$Kx33t+weTg=!WMGZif6)DSmb9T`dHnGA=P>`slri*|Q$qvGA2ohzQAvm=`k2VT-s65!CsBLDr(d1Ki5C! zub<-&r~fXeggp6$Qw%tM=9E9YNp$G1m2>CUs6$0%bz$a@_r4o#LvvqGD?^D z^8=^cZtMKaYikPUi9d5w08hroFFA(%n!%$#&@;b8{#;jk{TMb*hL6PjT!wRQ5eYMY zNE!D#k*M^C=b%)ZIXq>5W?Y(@c*MOwr2O)F=YD+~L#=oJ2DJgtqu!q@pYC7W{J?q8 z&@8$G!x%2`2^QIa3AuQq?dJ~dzmkss5#u>p@z#kWo)q;d;nbwqYyG?1RMswXZ`Y(n zZN`l1YX)^bV#}{3y({(~?f+m}M+G@A%?FB&DfpakvT zqWSBlzbfJ{j`*z!{d*O0Un2K!p(WGDIM)S~6WQYcHgfJKaeViE3OGUT?0!G881K3P0NB zn)Qm8<|Q50eXO&Qno-l~#D(otA$A%~;Nwzo_GQ zO+uIhGd^A5(Jp$ylnD2<%)5`VnN2%XsunG^4Xi33lXf!dw^M3RAxRXV$Bf3)zk$S8 zHW-s}oMRfJqL6k+7DQFG)XQgY_946Ekj+FTQ4PSeIyD*?M(6H)uAI2CWpHRY|Ayc< z5N^+lNB^>x|KnOzWYk)8Z@nquy?)>g9MTWEF~MF^XuaBNK$2g5u%Jp|l9<|vOK50c zQrGXjz8^lPpuHdmO=TiSk#pdZ$}ywD^(nEP^8NCC7%$=2!B^<3rY~zGd~(N-tV?o8 zWLdHNh+~d14hoCB#xt{(Ci$jKCY+)@`VqkHRe|=8BOQZ03X`MbIF#d*5@secA505V z65WLZ6|_I7(Y?y_t8tBqNY$bm4}8SECz%)rxCO~%CtA9Gu6I;Vyk(^Q?)c@oZ=ee! z2xFh+Qx3XBcsZ$!Q3dT85Tq{??D=WkVMR?nyiZ4GDbDJOo_CR(%e$IarvY53lBU8a zobf41rZkU$RQ)zrl_wbQL}TO#GB5@YAhCHs-@Bnh69;X%&I?fWC5p`#WZSR9-#iL* zC?tycJKrnK)NG!skMevSl@PBJtGF_bw-b9fE*B>ZG-dh$+pM0NK}i$kHY!flC{@8C zu?ZpAVCeZ`<8&jh(n;yH&99uVCi6bHDTlqY)-Was8x>f#-OlYl>KT=&s7`5FU7H&$ z6s1^a*nzG1Qd5=7h`A#M1d?d0%OLf3>SbB#i%iT$k#NSGxR0{co`{qjs!3tKl|^$W z*uP0={B9S_Re%8a3HP_k>2&h8m{+hsmA_cj&7tc86L%aDlS#_*hGiQQYGs zbunl5Ce~NQ$j`M-UQKBX@^s%8t4;COiB_GyA~M+TNHmJgB$&iN zjc0untb@Kxc41t{xga|IZw_7_o7=eW)2n``A)j2Id7Zx5dx6bti;O2=v)@t^BH15P z7iSWs@~B;WhMyIfB*5N_b4E4A)=R7e(pHaQBPw=qP-(r}jqfPi@PN;Dkae7K{~|(6 z4?ZC2nro6k1aC`w4MBB8$Ne1q_)vsnsz-AX+Sb3XBOydq1F+!@ayP5JkOCQviV*RNB=>+fAg?*I2F%i|9_EbU z7Rk?rxZ^a9OqLrc94Md9V+ofGlEotG2CM3G-X!#fsHSB;dZRbLL?k*9jiS85EZvW7 znN7|sQ&VxMDA~E*SXS6{31-?aXT&FjS7%YVaDYq}?Zxy9jZ|!$!JJ*0OjBW;qnZSI z&*U%kQF9&-C`3xhtL2B9>yP=Jrin(9!TA$ zad;!*+L)s6;_O(U0i80_HO#5UqzO@%9Nclk(D|L1f?394WOLd_+v{Y!Z0#7jaWJNA zUsEB^Lm19UR2eoL$?^OfNcxO0HjVqE%(=aWUq8CPc)IrJ_#5~az&ruG@9w#O`5kzz zAAWFFJ5`cFKVa-80WA_<29tU&$sZL%9N$N;3t^{H|BHN=jl9beq4zCLB;j__Kyoc zs2(}~b^Bjc_ZQpzzu#mo=0kN3M@KIwf|@@KG>nz}EbX=}@37V>9e>X$U;2VZdu zd(Jup(XbJBjZfAzd;aB0f97wdg<8gN#6$13QWM-HpY>ary?m>m&z`Qk5!Z1bgtGhR zemrx1?}^jPz#k9g-Ir%A2ZDgVW`IiiFXF#y<3FA^4or(`7&gYjWzt!Zu(Eu{B3NAw z-ZMEmnNqScvHvnR3(?xm&5!NWLkG8&$F)^c!~v$7TB61pao@g(emh~|v9*%5y;%pn zMMlM3M|GWAqBwQk!p2<61bt~!O!Myr)!O6$#1+LO!G{1#0Y=+YQMCEbM`dl3ge%c znB1sF-*|GUteklT31u5~PKXK6l4(8_w%RzT*{Kp$K4R+yOBiatn%VvI9?!ELobpm) z*G`b7M`E8&e7viNPO{mTmT>NsVhchQ9-DZQVHI=bX7Lo=C)tx)d9OXIi$(&*3v`B` zDO!m|!sId^4+pVC`@gNfQT%}CZBH=|sUI`+u5_tvSI^%I*vTwT}N)>Y3qrV5`>JCM9vFJ9*PT`sSQ$dICjEw7Ufwutrs&2 z_4J||-3Zd*r0{jDvdeEKegqs=L+e$~o~G~ytuLRL|1Q_gK*?k%&Q4@xL_@_KYrIw9 zY4yzee1=5~vd?(j!UKxW#Lt+c7|lPTPxe;NujMe28Z0dhlOg4`RXN!G=Xl#yf@;9f z`i~ZK*-0t%lTv9OmlyNvQ={7-l=lUo1FZ5FZuRb$fp1C-SUD47w9*fHn7emg$4@+! zx^iGG9C|-?6habNXzdFU{k%#6x$R9ESUPpSfx|?8q;&SIN`z+ol2BL z-c4I_c&yrkmSfUhfrTbQlU`TK7`oL*HXpbZ6ur0VDv)x}!)GODS3FK^GaDBirmTxB z?0pEwtrJn)3I-UGV5MlE=a{Np9KN1Ng4M!3iSSvuQffZE%jBBtJ>a#GT119w<3f0C zgN~c^4pU^y@YvZ)Gj!+dcK;OOdbV!euV$}~0XFO~!w@h9^SQe|+1hy#E`}P`wY&^A0Pp%y0**ypxF??J*y?T20aOQTNcbR@) z{vc7=4_A36MmZtFU9{n-{|iL5V;4frLAZBV*~6>YA+LwX_Ru&L+kdsz(tiq(`Eo&W zEBLa@?m@#4e@anKq}4&LHA7Tn5=l%7PokcCZMF94!S@3uQ}99gtH7%)m_b`X!Nk(E z056)qJzvHWl0|Kws%`O%N{)_)AD(% z#Du?*y8HbQ5Bj&`xc8f-=YHFeN+N&XkZP1)tano?L#vvZN6lItzjNjMz~o%_^auKZ z=bZAN%dd-mc>)+y_9nHzqsyjaeo}ZGdPVBde9s5s+Gl3&2R}%Hei*NpF41WoVzl7-jb3H?TC`>-^w_cOdmk#ur)j~Il zj{{xoqxtvW&uwVf-WEM|@J{#!+Lpb!GrbpO)-NUL^dXPB>J#X@;bko1G8?d;Okp)T-!4FP_ax+AvvIa+l3D zDBiE<8P1pjm9U+ZnQ=WfdTPTYY5UI2=}XwTW1ZR`qKNg}_uk9+!ZP%-(z*smUF!U< z<99BVR1kTB0*!5kV-!V%w#LOd`Ucfn{mB}C6??tJ?R8!mSuP9oa$`!WYcDug%xMtrW=mKOCvfVZ9X+XMCelmVJ&IO zDniu}vN<5`V#gRYF&3N3V2Hgs*4JT_qW|;=<*Q!m?NnwMS#f!SWKYKYeOe6;~3SPp~!=`i~~|10s< zFCreSR2L6gU^vIAUhuq1=Aua*;mqcz>TWH$g3pjf}KD~a`zEZmi6!rJXpC9|tJ-@pI zad^g!elm>HZDJ&2t>rVyBAap|s0eOkN+d2V&U$I6QzLZy)75foyW~=5!-m<=7k;Ai z2gzJr;l#lB@Jmqdb=rwn>YkM9XtB>pZW89EIl2{<30Uc5S5KIG@{*D_|ExQ@%-w=$ zv1uZxsg^6Q8qgaoU0)Z3p;|?La0v7U=M50Xkguo-5ONd5F_O1@3oUtYye09eOFjZ- zm_jwzNnlhW8xqzKhX>2G(&vaT-~{JpM(VCmNpjND;v3L6o`^VW&5I* zx6NIj@Ty(BSM<@X+s&+7?*%?*{SiX`m;DPZ4u}p~SKQ+Adpq$6zgPbzBPAa&u`5156>z2B~#X?pqGkB1(U;DL`0ED8) z;)JKzO8^^wvj>+*1efB1p=<(=);|Svs2cfEkR?U?zQx59=@@0|cX#*^gJ7)YEi=jJLL!$JUiVRxT z5^2;gVuF)MP9R$N@Xb*UdSM7D6b{VF!t-itT+`@6CfXe?D_W7;Cds3OZXRAQqpkps zlOVU<5{W2NcRwWnE85?9`r#$T4*!7mp?hQ=M@1U)@QFpDP@G1zq`y=A&LH=*bjF`mmbTke4@aHwK35$*|I_PkYx2rXtSW>Gzp8sqOQzkVC5(Ax-UGED1jYx8eLzn?-4me&?%c8!sp+Unl9=tS-p z_v$XsB3TWX4$SqWG7gY5u%|>XokpQ#+FU)TTL+HuSVs97pR%~^hA)elm~5R)n6Qn_ zblI%N6Xqz+Q15AuSV-2Z<1Ovbh#6K=Pht}{+R&icU&@?P=;&ol41Amc!`qYEua-}W zLJf0s!q!$waF|K8I|n<-onjB5#`a?!74}394vvw&2~2icI+T#_XI-cOd6k>>i0TSh zUB}AQGB=3)y^;M7#P(Xk>SAK>MS^-Jr$w4HB!ot?2!P|xC+kW2Sy)F{SYTm8I7EXb zcSIi9rkj4D^RA=S zP3#%W4^?lP#+`KrGlj0-Y=@mey?XP0W6%VtLF5^79Wb_xAtHp}*N}uNCIL#M+OSZ$b}8>sMcs5xtqw zfBn!CGFDh8Ch#RWQ}Ph9wLyw0R;EglYnh9=s57D(x@cgfZ%71Jjl+5ghTXv>aUIW1 zv!Jr?2g1VxUS`6KrwvWAsz_CyUi#6xlDv91?h*$c&oN8Xu-Zl)?I3A!PE2B|dU8hF zrCy`@UM{;IBll)N@wUo11s_XTL0&(;L~!VF4i3LHG$q4h)mDG9GKYu34Fy%eW?D$N z6y9llEDi4e%W>|Pdasu%C!*@w=SBJWvP*vQ4evLQFg5N}|B{qzx$7d%0y@~* zN2<>rj(suLZ0)R%I&B28iwZdN;VQ=IJ&%b*g`YQ3&NR0xc4R7X&_P6?!4$ zdSu$!Vlg}2PsO=QFUvI#QC^#YDMod-jFnXR!M#y6Mp=Mc2S3$oN(=tuORBH53vgqV zqNXJOT%*!ty>L;ryVZ@3oYnx`vEn4d%8WLdS}|}86;$2URS&M{K1x)&fZWPM@B!vC zgHDn-%eD>qm!_e3M>N^YbcJP8a6D^RW2^}|BlqE{ERXCQfAc^1Ak8F*zK)y0@ z(hCnPC5klbCF?p&zS;xUinJt1soXX{oXy<}x7&N=Z+ zP^D`5J0thb*mZ`b;aN65iPh(c&CPQk0_&U&QahWNl3|(5{U;;v0vo}vKf`pN%mANE z|Fpr{d;q$@mn5izw?Mr{6Z}M=LqL)-qZ>^Y#*H+xj8kXbA)=S$k{{3Vv4TyG`zFo-k=_kSjt^;?#(w;oPOfv zxv2BBOl;8aGW_lTKy&|F7Bt1V*$a}atRoeCZ1VkOv7FC%1CNQ43+6<6iYjA89gh_c zzX#yrSIJ}UNRt-; z9B|B8cn)`C0o)1-RQOl63a!4OI=3Xh?|T}Zm1BV?UCTn|R%_|Y_+N1-vrel-iJsRF z*r%ugSx^#hSByKbI=!qFo?75+rVM8#^;X4nZB_T+WGC{fH60xsBOPt?bA|`97QxM9 zllexkaX^pj!_t7g`l-B(a;%;AK?|xjh?I}QX|WIF>zt;d(NGn0ihS+EfO+qeu6pHI z!aSr|c1*rB?YNtCRmBz`3Qm~L9Wy36a;eql>cLABTs1wWBT_tP)7Rt|7kNK`4Z@X- zu#znK^gSM9IfmlpuY~!|ZUcQKc3Ds*=9N)(RJ&!X$ijBO>=e@E)ndl^fpBGDttUYVHxN+eIdO{K<%cZyVwO^N8{)BRcm_@eY8ENANBz&uC7Eq1(e$TK7f z+?zMI1o%n^ORF*%OE3iDkWk8&JVrTYzLdz-hR-HSq_JQ;zJb0Z%Y9G@fc1#NVx9Vt zTWt)oYoB(#p49V4r4nT`;XZbTdWUnk)Us=35-9uUgMfmzA2d`u7 z(!3h?tNT`}u=_Y(2=F=ImufD|JJ1l1^qi{oo_uJCKEy1>o2*0N~i+4{$IHFavz^^@(pzeErQgC%zF7_~xYGcY=baP6-O1 zIs5H*V#4C$V#1=Lk}?W%l2Y>0qN3kl{a*fxB2XD9A*XsxRq2|75>V-nNsb8!2ne1O zydWreK}k|nQt5yBI`{=}?&Q~c0zV!fG^j029DI6#x*3r)(kK)S5uF)|yhu0S?85kFYbz#Jl;ua(h6u z;a=JPJYwIB_3X*An9cmy6p9h!7^WRnXduxD3yI#Dx3EG|5rDz-Y37yhYDsA?$_|t zd#-7IquKQGtP?wbc>sXkfp1Wv|nPvyKczW1OBs> zA$v&3XyK7$iC~YQ&{bQf;NmwL-w0xZRuvSP9E;9u@3R2SZ={r?2{*pcR+J9Je>(Q+ zTGp0@zMp=;aZg*S+<$NFlon_dAS>Cs+OX?V?fR+eG#Lq?Hy$ zBT%+-^Xl2O?9Q?acPE49H%;irc423-4+kdhnTW>V_j=nu?hgno-|6|G_e z1Wop^_ILbbG0`>p3y$T}x!Jw^{2_d^1uJ$dlh~+Mw5pjtZxZaLwf;wQ(gHu!?e(iV z;cdCsY3rr6F@8&t5x-HhPLphNwq66-IC3knB{+nl$0A5)BDfcvKCbkoh>1bJ=7(F) z9-}pSi}Q+?9;tO^LyQH=TL0$}_kRJVRjaAm9e7>+M(|H>4|9dUauc?(hB*Y8SQ+mm zxAs`-GFHY;>|3%%V{K~9EOZu$Qobc*r2%Zr6{0>AG)3)}# z*+ttK#zJq{h3!mB7-8*UvSa$$NpG;$oKjEM&hN!Gx?)MZ8`(`;(01QPGB3&JT-~zG zR+}!opPbMIKio&K46dx2x$Nq^h}UyuL)h^?_ANU#I_XofHU|LffO)>!KV}m%srsA{ zo=wM600jLwpiXJ`_%_v**sH$Q3ev>fM#;_d$5TPj1mo{&|x*<{fiSmUj6FmQE_7Vd9+DL%f{-va%_ z+UIvrgtaL;YyS3d%MD*88^8A(H#;85z5F>kGbW|W-_=L$_V7q7a_kOfSC!|ABT$MA zerKE?*`z;lR4^$ZWb)+SR#!0bDORIQ=%x~Tkl;zBDShTLZGYKZFmb!>xL<18^t3d0 zLD`{Cc=`Z9r*=t}R6J@yQUfP9F7xu`)^%Tu4Q>oODlVl=pW!_~6SbN1qM=h@uW?;q> z!W^3{bX$SrYb)_F<^2JmxN2K1ef=AaKGAcyQ-1K#PAyL`!QYvzAGJURHhk_aTX);i zzuUW?#vlHzuSoE_Dointy5cwQ=Wgx9n@n&%t=9^79$u^s-g79QD!~P(Cr!gJEALa)y68u{vC*#HwhV7YQ0Pi23qR z7Z0}@pg2IN@X9v@4vzy{2;7QDo%IZcF(YL8<3t99G+tCmQ=#5xgD-h&)T5d{dr z1)DA2sTbha9Mbe_=iZq*!!TFq%;;8hM}EMf(k$)~-}S?TVVt=*i>lPB*m{K{r)eAf zKC=ALketmZV}jU-n03V6W%-sA7SLVbOAi?aCGeMzqt3sN>fRQtGhUe%o!g${mXi^v zBJdN6#65KfEuPx>U+Z5^51}6}Vg~$;uW&UX4ndN(Az$cT%_AzO=HYsSPhXEC0Dv`Zr0Wc9RKT_$a2 z+1uxiO?w7)s${@*;tIy@)X)rD1K_@GdEQofweEHSaN8%lSwS^l(iLgV73x>XkRnIN z^R+l<+M@w9#KwQxS8q2tU|U@%w>H9*&lhoEnU zu@qp>E(@E-yRuv#`C!4&Bl6x()SgpYs0P8HNeX)M}{Rpz(ID{^PC_CU$N)OY~^A~Rw_LQ zM3@u+Ph%eP7&#CPzh(gw=eUd!VVp?>J+lD4k=NGLOtyN-HZO`Oqg<&T(2eT^9g9Q^vPUUEqYU-|;QU>F%FS)y%JNiCsIvCHMt185RfK>b zNSI%`>}MXt`MBJZ6u{lj3FJzC-kiZKMKRKt8~7+{Sp@~2w6LgL0KzUDM)+Jc&Z*JK zS|tS1`xNYIyMoVuRa?Mb5LB-8tw)3*?W%G=lPggc0S}!$m=PNhrzf>Ak{e+g+NfDa zM`0TLahw-^1}Ec%N4)peu2!HT94Q8lgT6$e(iY~{b^keUr*>Y~#4?R=f5r)sG+x>? zS+o(o-yCuy!UA6P5{H7Trq2rLGl6e|1O!Xp~+U5urUMGorpR;4L+ctU_p|dt3wzq~k zamwscSfi6;5v)X;q8}8QYW;ZRMVm$wqW?Wi12Y=DH{ED_06_E`dtkRs=NNn_DGWB~ zS0o7`AeF&))5i1_<5wyNWyNQV3CoowKU2dEYuiFXOMop;eJu?Q{8T3wN3tIqyyvX? zpdVMF)EA8-`soyyT`twyHIL5K7_z#Y>Z#?7RToH+RVx&EwG(_+Ka$f>!Kx?4gjAlV1pAVxr+ou^9KgYe z)Egdpw&FAV4*zqXhQZpZe6WavPbaW2Q9PqvW>oRll_G5VkUAd5aZm@&jtZ$n1cy01 zPm()j4A@v?eF{i7H0ENI?8t)l?&8omdk^vA%jsXIoW?d};Ah$`XM7B=Rv zaAZXN=BCa# zDO5pwWJGf?zt)DV_MmXK$Vf&UyuH7uMJg%J0ht&%(Z9sO-NJ9JQhxK(N2Wh4Rh#r$ z!l&UL4}37hrVcVnnuW7Usx&`F1on$4cFtwoo6hqD<8tYh`4xNIN_t_(9xuUfC1oTK zy=gM9sI?3UU|ksvl3N~N{jJbz*qEM~=S2x% z)F%G*J_(&m_%t`fPx!o`t^KY zJm;83$Px4{hWelPJEcS~BCB2(+LMHOoYQci|8BasMMw`WInCh zT6>lDdt^#|_7yG1J+a!UJ*hBDixCJF6c{&*S0s8DuMDHA(}@#aC!a^fUeR|US5_sl z&eznFEgn{)idve#xS;YLKCf*br_-nK=b?x!HyK#8$=U(nhfzNFhyKRq5SjNyu*?t= z6(>>C+9_0hN-Hfe0Js<}pj|3Ik8r4|qfpionW0GLo01WwT2q|SgPWF6hy1Bwl=or^ zBF#YXnc`ZswXcZpAQj$dov}Xe;YxFA=(Ihhuyq-K{xb7D$2i!UpP3nL_*vL@_|DT# z!ob|{K+$V3I>9LsWXPOkMoUcaWzNDE8`-{L%0y6da$cyWG&G_gG83yA5bXThi&Dk3 zjSQQ+qxOwVt$cnT1!(dV`z81zhwo*}CoNDGlV7<<}kF1LaH7@I$1X-*^Sj6wf zR_14<5k<v85^Kh$e}>eiohBn`{qSf<8!RUfSv9cz3F$Oc_Am*?A?-a_>jnz0+c-U zstqg4v(Z?{pPRM#B<82S)3-SB<`BXGpbmQ<3umL+L&El@EmUIMI;)qZ?YcNkjv5s- zDG4SeWP+{FPQ=Qqc`=dnFP+9x7AuLMky(rpB>yFyYHP{0h*wqcBl!+u9?i8oStMPa z8u3;XW%&iqKH52RXSR2NC=f|<>505M3c)GDpMU~wgON>9vJNR%mL7!Mx~dv}Gt5GZ z@KBTD7a&78?`LNYWw-5qW=#Bp*EjNp>39HO!880kEvj%Z_KaZ2r4esE{Q_0zs!VpW zJ0X8)`o_r>=6<$(p=yOJcx7?T zM)@J!(27lVFDq51OS>zt`xg#ChZ@=)Jf=sgqeia1>~MVXaN`;cz6x@4MuO7q1LR{B zz?u{TV4?3dyE|;Fu3*Ia>qq*Jve)+aZ#=E9r+~Th;I^omv1212?43K?k|ZLjMfI&drEk@t zw>z{zxxA947Z4tZo}G$S@-MBG4s zo&eBaU(@!74}2tqX>jk$1TpIw&8x@;tgT=Jg$%-9Cb`oTlhj|YF6}v*IXS*9w^s`& zbnR5hl-Zs_tFNAeVVgrW= z(9wO72J^GEeIaY{s^^eGny#4WOK+;ymEJ?nL~R$5UM=2;0nHANN|1b^%=9st@WZH1 zz?~0@3+3TXUKzHtK6N$g9q#{45z5{Ab2D3g)yqq``Sc#-0HEU~@MNzjIIn4cWBm;J5m z^M7n+&jkRr1{Ff?S5~Eva(#W~A5mb$QKvR~xV}4-93QvLBFsEpH~>6|vQ*KR$_p7( z{uIJ4c}*vsRyM$@s(q5_(X;RJFGSp>MY$_tS`2v#6UdY~*h~81+7A_is^~M&S>$wX zJ5ZEHP$~-PUpG15>;zKUW2YkqM~YdJis^MGxA<^LHMrqzijJ-!o-(VLMD@x2qy-c(7*Gm`bxj*)%_H16r zp)&cYwJe>mfl3^*b3!{Lc=%I>ua<|S@4CCpFgCJI{ZyCb+G;vo-zCgoLW~IJq2M|jkvfl-&PG2ldVy9<9?m`E!L4U z%QqHp$J$K3(W2wn4D9B7v<=R?_{WtEeYy?{YuFM6 zn7yaCxz%B6-s6AvX00Hw4t{8;8UKvKK>xPpZMSzec0+TP)8R?5`YwD?vZK|+z&@dF z1ka4P(v%*2Si!jIhLpku6c$A-W2tSDV^`q`DPq09?)Z&LdBhYH&8zqk6FKB6vc#P6 zE1rMsi;PQSB3;H#{4=&I?Pfkhr*DlQ#s4nWSYlfp~{NoFZ|Z)9W(U6HCgLT2i8F8#oI8lMviT5xLXL51>{ ztYDlG0^Y5QxZo`i>i@%nNrC8Y{$|62kf>r~Derd8!9h;%!|Pl<{pjxiozFV?iP1*U z6yNq+Nccm06yIDAl$uq6M??;Oi2c%Acw?KR-K0ZnrEy!?N%pyXYK-*c?k}tQ>O!i1 zc_H!nubSgOVS$5jlR_>o-j@e?EAq1pDJhA*hSE^iaSjdk9c5D(Vu$t%Tl5Y*&Y(uZ z20_pqbndKk!yN)_v*T(_N0_&dbd+0#I()g1{8=AszYKgOv9hE@8Gvxjo0?sHbzhC$8XXyh)-=}E)FfRrNY^G>7D!fYB-Lt3hFx`zWL&lOC@q$cp zIJklBo2iA>RCw-AUsd|-tjYX7ov3=;Qxf<)(&oCY)ozKHgPMvh9M$0WZZ}!AmFMn1 z+iA3KgA~kw-u%FIjB|(QU58x%n4g(c_NCJs2a%@L)^3XEXvD5}(9LkWBuxgV9Mxpl6-a2pwxo~87P?J$cLQU@w#oYs z-rG)0&T8FKx;ra4K_gvLXz(Zb4QKO0!|XnXK$09HLq;ID|e=>yg=;KDvXyFWwlP*k=#1C1hjdJk@{e;fRB!vFiM{hPj1~YdPR^@Nqi; zc$}SxRm|o{KRy6_{wDqa@a5XRV#-?PKEWt$AP65$Y>9%yBI&~66Ktk2_pK?PY(UffWa@#!2PMVR7L z75AP}edir8jqv1>>#bMH+6DTuB}n?`K1-7+dH5H&N2Wz36xXcA?o3L>0PevOrMDktv^3`eXAPORe#%I^LsCc= zyc6ZE7F(rze>+;fV`~gFG(lU4O{{vuXM)i2Mi23o?7a#Xyryc+V!;wu7QYVt#T*HSB-;GTN_O2fQv?3IVUokI#%@`AOsPu?VdYi>zvern@!{X&=it--S@w~>2#mEfmE;HorAa5%W6 z!QC4SXC_^l80=z9Wb?V+dpDTd=mUT)7@t~GOA8PT6=vVQOwyuOW_L)NWH|2)m+#}7 zFE1=sMq{M9GT8GU`s}F;Nb1w_WbF~*>c*u+{YnA(nH+UBHB>3p0B2t_9EiHy-1<%{ z%gB7v+@?u)AIHy57+%oIZkgYOHRXjGOCj5#o&?`TMzhS8iJN!Z`xo;c6;1PTwL_rC z^lo6=HG5c)kYhS#VFqCmBQVH>V#Jlpp#{0vJW^ri%%kZm&$KHC3_d_>fqyp?2%9jH-bN(H+A`Tb6-Q_ z7312%V&ALhZD{@$jq|*m1ZLfC2}`S>p;Vqb44Z?rFSO-{KAP96U+s=AU0br^FbzcQ zYSXZ@)=;Na5|7-$urrd8`EK0$z@lsq656D?lMHe)UmAH{N}8eMd0|umxzU_FF}|d0p&EC(k0>iGP&}x=7f(A$5s+n zW>R0&J%>ct; zE?4sxddLY!(p6QUo39}#)7+^aj4Cc@((ANMv^5fN!dPcHFP)LKl@Dp$-B9EJFvHvr(xwE7r0IA6Fk+mLeORWA{d|oX_M(4VXYos+Kd}+WNp*%N zpbh{pi6&Gz<^kZNHyo?dPX$%l^9K@>ayPX&k|sCLX6+{a;8>ql6iPna!3eXqe1986 zY#sHrqta*z!Xo;ust(fn6A*vvppB>(^>q# z&=xlJ8Hu)7VUSx@h!UjQn`Y-I-taH=aSMD4 zyL&oG5~Si&2&9cYw^FZ~G;hB|bw&&;4_P#nEEFy9ot1?>3fHBjorSgG$Oc~U{;l!w zN9$WbQu}v&yh%kakiJk=W$s{2yd+BdR6(aEx+}@!IncLwM%)`EHy5T@1m~239U&h; z*oQ&+WPRmSc3i5>Vz2Imi&QT->O}=j$*^v{i0lDxLl(RPi`*wG{+yRKS}$&?AZvC9 z6Mq^sba=4BK*YyBf7IZPpQn*e{-g+vaCvyFCJ?=ysgjaVK?*9TS^AwYBKpiR^rus& zz0bU2J=~V13*MAE#9DIY)67KUFWusPT@~308~=y=Ki2p^VEC_MA>c`5W@7kgx6aGi?8D=e z4}+IoCtY0#yN)$Nc@c_J9!S~eTjGQ1-XNPl(8RF zs>X;>8Trd-U-jsurHC|MI5bIp#=p`@TSLA6W@+GGx#bVyQR&~uywe)SYiu6}Yxt`G zmv8;ect?r9S^FEFcD$JMGU~M+MaK2PKimKE!C&2vfspR49c#rxN8R&R)A2w1)k4sz z=)2aBuly<5zl1rhHGH^%$^YZtzX^EkUm99{&L5oaJ1WvoSoy;0rGQ8;6D_G0PKrpf z(%-Oil=#aE|Kjz{QN~Am{c@D}`;f})lUmyd3p;q(?Z0U&@NtB`pNjNB`ff-QDd0Iq z<;bKpQZkrK+}z5rQA5ozVsp874*+Ec0PbNSnh^^=%A>cDylpwblO49)RxiKjCCYl(=9UIx^vs=C6 zi?RB%wtO+5+LF7Ff+;b+3f5=^i{t9A+at!dKJGv3|6>0@E4mTa5M&y!$9x=Z{}y#r z>ulZjC^BubXvUY+f7bTd=bA^6ANUurqSDS~@AChO{iBGjUFD`7)!=B4k4K4rrIVw0 ztvSl~uSx&!ha9W5aW`+jf$-85296-?tBa(WRIyW6Cxh>59G&K;DT}A+RUo1^GW_JC zgP#LVf_?yau4JXVad$WI0N{K87^w`~-!a*@n?H(|9yqk)08kRU2sb zHdJK}08PCI0Bpq2p9_kAA=mRp_V&y^kJP;J;Q+AmY%ll#fbJjL`E&r-vZNhN8>0U{ zX=)C6@g9Ewh7U}AUu02SWTU-dZZ*`GH-7n~6*BKMGM^7m+TQPw@_C<7TCp`AId$WK3L&p9V znYr^vvS_YIPuS4h&R1Dwer}T8I{^HCI4SvX(xzTZHtvx_UEr3i2Ux<-NVUmW;PBYq zVQ*zr?1x^~2*>!sw_eOpuY2hZi$N20;a!JTas4ZqQvT)j2*Obq{S68~9(Cy7dI=qc z{n1|kqaow-Hty{X!Ba!z6%H2In zj6m~Zy<&^NdH!T=eI}(2XyMHCb+Yj+4%W;xM<|9#&9vpFQQGYbGBC z@0+HM!Or&jpU4PWQv7gRY9s$M#~`RXqRtOXUl7IG`#6BPSWk(8 zHv=BH&CF_KSF5?0PPRrZi4&KJRxJ1ucVmxnpmRucCyyBDZ(b${I=qf+7tIb=ae)k@qx$ z5`$*D^9C+-TJ>nmGF(STS2$Hae=u_gRXD(NR3ifK1PFO2Ms9shSZ|1X6#z-C$O3kH!LVTd5U=9rl&Z0}8l<(Ok zv0`;Zml{ZoQ+5Hh0$phNAGpYX=bLjcIT!I8ar+z6U{T9y@c&FGh?htGK;UGZ~;GrF}u$ z^*g>WAsPCfvAgOKOBB+~AGMCCoCnbg<3i`D5Ib(}^ibr!V{!fKhva6{uM@#Q3U*L7 zZTwTMqbokXsgWrrOymhl+5SWkX36-G2OT28pXUayOVdh zJpF9X1=ZVas$-VF%+Rv#-ddGy4qpHGNIIduskv`)=Jp`Q8$t@nqs~RWa=V|kyM)Mi zx0La`raB!84p}8A_5lQcIkrN`Wx`EaHqabo@JWlqy&ou^WK&?B&{h$_%>P`QcGWzk zNTB^LzpHa3t4QC*p@!I%BUvf0zBo`=Gp~C&y{l*IGO|Cz&3b@k?+}r)V?eWUslcf^ z%4}m4nx`X*8X$;;chl(H{lfzYUtTsg)qKH~Rr{D*~&@3$Jo@|M=JFT!3ppnQ;m_*Ja0 z>e{#hHWRV@K4R<{t~|4DPFfGPOmlYatM-w!qhid9;P;xAKlF+u${E8E^t%Y-r7Qxi%X9p+#6} zxQ9W)I!|E{E$q9ycT-F@s{_aGU5)6$)l`q)pUv|28C0#RU+^!1J(uWnJ(%p zx@gBAoRP|FF5Qcv6p*c5AxHwG+Mu4f8|0_{L>RyQCJ6c5*|di-G{@~Xo2J4Wv?s+c zuhy$9v}OatJ5_jCP=qGc zVaB#bAiguReoi_o3~n%i5$q2nJ9u7)3ir%~WnE`LGM z?6%MFqqfY@s|pbR^8uO>uO_9vlQo!I)vMlhUDnR_{pq@fhKYUwo|eE{kkO=8+RAKDFNZG}ru|^fv}J-w@3Mx_ zK#1Gzu{Y2a^^0+>DsSXoJE60&p*fP*YRhOU3uRCsta>5+;yds zv)_9gT2*T12Kw*;95M-_DW}?cAcGvcjrUH~b^Jh-FGPpzhzI-A@Fu5?< zw=$Da7~r60q7umw&mDsUE*+Zna5F2|5OI=$gAdFozs)Bqd|M4vDs9VXb6Uv|1CMbU z%Q2%1-PA#fX^_>O_rcgShCIPPVYKmnnJ9keV-(Rvfm}Gof+d6kmDxTz1@C}*JR?S7 zBq644zDGaEqTSLV9&Wdp5L{%CR!N1P@;v}NJ^yGI!G(_~RkMyyg(&Rfupx)elPx`YBHp`>}{=5rl$g@j(oQK8iY)%A}p zO@r>x@pYY{QmC6j-S4A>;QO1=Vw8{`>qWcY2brRd*ZK#LJ#It4GwjF?^M1?_?9|NV z)JKWE-R1Eri|O8d237GSH`@+S;OJ5=~1g5mOs{; z;O(gIw~)1L-t9FHF#C z5#*4p6js!mDkePyC6XtD$xkWrtfD%?u6=0x4xI?i#Afbtf(n(Y=RYpj$_eQU9rxdY z!nq~5EKdi-dzkQQ%MDlfjCZ;D;p58K#1>h!1BO7Ws&TpxND^SF+tfyW<6U}s?e-$~ za1HU#vzSU&gq4rvddd6;3%WilpI%`@tt8}bG<0}gyAVFvZa~5Ga5q3ODb%&zN@t@z?Ay;q2rksIzu`@eXWl2J;SbIhm3-$mJ~*QC z@Q?;LtF%j6P1!cmiu@9S-~MM*^Cf+RE$$8t7I&53eWL zENUYc9S8akx2SPOQq^T;AtDQ1r3HKxIi4!ySQ zX&I3X&^eH4N8tdT11q0gB&K9w_dYEWU#(TAyU1%+np^Q|BaRv({0eT&L#bh9MLK0a zGn0#kY?giv3!H4Q3&;~_A6sF->Rf5!vmb0NhDZC#YU@Jjo?yDCm@sv2w;yr)$~s=^ z<&Y#|M7|5PnNeNJ7)jT?My{?oo1PAw@5Qr2PSELb zBn#mg7#Xog^vcWJQkpY*pulkk+n98v{74Pj%O)vv`ztEKHqxlNH(7ZUrQI@YFfb|& zX=KlwY!t*+e|R#%M=ugx)Imf;>Dh#gn?5yTs3z~gaT+G8^kZ)qjYe4Y2>%jScn1KG zGwIx^ANKLt^x0+u!SpbRx*#xEnZS#GWlK~$=lxR$N#3MAIUDj$DZfdfcNxXp3=B^snP)yTL zp{jfsDG1&e`YnenCP&;&af|Ut@^=<$|8am&HDc+SZMboHGs%~~efRK0tgG{w&m+eM z&QNWqelX?o=jGb>VZwu8sICh#(^`c;>}EBrV8f1w*VxY~Y?i6vlIZ;nD-66HVO+X9 z+@LMUE{!RmmejDhqvza)fnUq?KUYY%ex6b~cC>R}>|vcQdB}$Ak}a!2i}u zKxY3X$@5faVd1(*@c^=8ahLhd_2uFBa)niOl_c6#7B(!p{6!Au3=pYo*Nh>N-Tc&q z%v5Ad(nn@WDW7~s*g3$C&dgs4Fj$0Pq8VF22&HI>!>Ldt%j03BlWh`d0eYh&E)sc2 zQD5^KZOu0ZuT!~m`_WIYt_`skE>76cjG40E zKe$mlOP1^-HX@<^6JZzd(=4368E+Kx!8UxXiZW!@Dnvi*c353|+W5ZHBU~eD?Y@mO|{<6EhMy{o6os;X)A#3LnPjc|4;5O+K$x z^zam~C?gZr$x`hBl5~_8?vGr z{3jN|yyf(hr5&3;t)w%sb36P+Ehr`|cu$8K9x0VcQ&do5Q}qfpsJ7>@-XhX%)=#T~ zw@SWUSF05(i^5T={k|+wb_f*tKTlcVz8r)uTN70|Q`O9DCsNeoZe=|aG`~s;hEM%4N8Cn#u zesVeZZg&~ua~YR`IU>8Pc8a3ASD|v4gCtR(`95J+!&-vxCtI4=nXafR4;ym0XpyJX z|DmQ%Wv9bk)oZ6Fy4URpTLkt2(zh~8*CXuu>grUZ%4-)}E7wt14$W*FXH{^M^)$4} zL7Ys;TiHGvd}Nr{y`9kaj;bafGJM8kyZ@BVB&@Wm!s;}2rcYMt~ST@-P@t+u#w-cVkLkSG0Fx6TK?LT4r z*Mv`0Yi~pzeh!AYyo4MZxl>fYZ-piWrnNmPp5a+ij`-_%H{}ig4!y_^1+!lu8-&W+ z&`?$uR#n;ist(yrZu%%K5pvT@T3C7RI#xta*-AO0#8zc-y=L)aiuDnzAYS(5uc3sl z=ayVmBe0eG-(zJJL^F`3bx6%_QpeDMLZ~Y~qY<;${cWvgUh2T#%t)rN4#zMb0m z3yFV*%&p)qTWLzjm(>Sr3WjHF?fV~wdcU@yY_Jh%?DPm(w{T*k(`M0c;mPFTx-CD1 zgM-%8P*jvy={+`j_QBX7oXAS?4J)g8YoKR#?oo3{Y@%+=hlC80d$uY7 z6RXSVku%Y|hp4RSU2R%*fvtL8a{Muitt7v0~b%qti z!M;y7Xty;n(J?v_k}v%&vpbE1WOR`rZkV=@De*2l;$;$cLO-8_o3iQO@eWBqjjkl41$@{13@>sqHw!4?il5sBk`RBD8d+NFJkvXDC z)dU|1=sTyx)H}(#?%Lh8TmvW~1=~;fkw_bDZE3JqdGGjfj_DK_gw4Q`>Qt1$YS*pY zo6;7){FyQS^pi#K=k0Kwgr|)5v!4f8Z9#334-lzcT{E743*?7E{~p<)qI7+8t?~?R zdBfQ(Qb&UQ)g>RhngqviG^d*KbJZ#~)iz9LSDt$$e+q{?4hf1~pyMPLlM0{%0d_{Q z8JY00oy*Nvyh&>^QhJV*sv;OMe8DU4^C&pnOK-A1>_m6Oks!H<)>G9|r8Ty;dRC?t zIXGA&)ziOA3TlrX?+u^jBocuwnjt);N{a0DXr=Zen7+9hJmr}?#eLQf8w`W3_G`pj zBWir>UJ{`2USKN^}# z8wX5zTb@@zp|Vq3>{r$jZDCo{N1zF>n!0}!z2E;EbkCr!lc-39Hp+!=jTB6^-UVsc z%51*t33@~Gbw&*2h~KfFFM6)ju4WTx9otzQ&@wi6)OeSYt$gZCl*vF=GL!7&+mX+^ z!J`mp3l$kN|3Gl!C4-7!!UcDJhdfSevB_W03cfnm9z45~m48G+^o@Ge(<+keIXIUM zq0#ggF6EIOCN-n<)6bl2Nl_d}g+aDCD-BxlZZx>BhJ&LF(})ofFVujo2~#_)thz!? zUUvx@J(y9?4qrbK7~dO*-CFUTymeSzw`YdVEIW$$SH?$_J)s8aLkAM9EMY)*qu+Od z0XBrtErEd8<@Jy@Y%^1=+8#J;G3`s#w~JF&emQU_Avu(ZhewHBUmrdyCw432Mtr{r zB@|%`O(xXUQYF)9(H4ZVqC@Ytnp*%Bx|99<{{eSGLnw-so_jr*ctp5e(P4c`g9_Bn zNQT#P*ziX2C_Op1BRNS-;l@7Lr?5`86>i^54Fic|6qJRj%68V0RA>_HZ1S^9TCG(C z8o4-NEy`a$&wp{2SxYjgT=%me*jMY(^$M)KWB3(oGDjYtwZJK~hb7WbF;9hGv>g`a zh-16JuVI09332}BIIQ&}kICulpvhOGYWasCS z#L0#0uCVf`z>QVek|Q8Et2SmDr7bw%GinW?o5S)Ogfp{(_0_~VqhBm#j6sn2R)Yh; zcVw6d6*d^+Q9Kfgrq|HCMR$7N9LWWuQ{l}YL_FZzA(gmRrBVA(>5GCW#Uk<5*5=P^ zg!QfK$RJZD@x6nwBuA$&Ex)2(PP3Ok{yx%(c2sX!uf)E$Z|g{KZTl*F1#@$#T^ANh zzOug%INkBSD+bI(Cle1}nB`45+;~d}y|J~Z6g(5wnUA~esn{%3$oSn%(g}2I2OPzAVtK6kVrt0mH?tSfP^7HLMWjPB0_@n-h@%W86Yr10t%rSLP#L= z(2IiL5Co(Il-{d=G^L5}7d+=U=bn4cZRg(m-uM1blTG&CYps9nwbt%y-DD6M?TO)- zR5g9Ptd)^n$E{+O*8|l`{kpl|yfQF+{M}AUtvx?$A}wdaH`Q8K`o^y%^IOQ# zN#;op2vBJHeD=k%wl-5md`1b}AoU<-&-ukITbQlah;l#56^TXPfF8B=QtQz~9t$z< zH7#c(U)j_n(-s^3YVpQ>QP@0L zNfyR3i;`AiddH9txB6Q}cNf12%rfoj>gZg@q#6mZN9Yktwo0p~BEOLxGs^h#gqUr* z&voknHxCCMQ>8F3gQ@lcL{$a<=#2e6{#B-haK0O^S<@WYM&`3k=L1`GEAg!Gm~%2_ z8eXDh)-Di7SyeXGKd+R3tEuQw30HoS2$cI#Rp49M<%b`bO3=6_VS>3Oow<5t2|qod zyxJR({_TM4H1m(+tY4HP(UZEfWlAtaY?_^6#5t5;or$`}l{ut^c${^^;h)=Ljh4;B z=sM#p2~+&_IS%SUBaZs2Khy7h($e%dJ*p<w4;l2)um zh~MMp`I|K^9=w+Y#6^tL3dmv~`+NK+EaF?MyPXT8ULZqjhUHdLiYqSUCd<3Mc=t^k z3#`4H6L0(sI&nuu+3zY6+k?Y*JxXju5mauLGja~W-!|J+I+-RR@tbi*2(py@@X^we zoW=74A7fm#$8{W=mnJ>`qWv0_7t?(Ms*iQ!YZyshVuiPtuLe_WuxSv3;AtH;56=W0 zJOr#3M2=J~wsn6v64J+w8RB9*!kkSb#x8gIw8nnZq!Y2}CbqB^w?_Sl9~_qD|Ja3H zYIoH51JnO1fA{~-{xWqq{r}mrEm8lw#6jqr{_>i`-tC@8q8tk>D_nbXV0%%6u)UaZ z2eiAsJr-T!#b+i@0A*w$X6`h(xljP+1;L_i~NB>eY|sQR99oMZ`4) zoFQDRIk6IP2y;fDKC&v4<1$fyDYoci$aAT^OmX3XS*7?`{Y-Auh>-iqV&+?iI+u;x z8%IpRi&!KjND)-{mr>i#~f`%{>7I)wM~3(7WM|1}oNRD+y~1E21R( z;(_(wv?V}Cul-?(W@mv0hGYzA(57t9K@c+3HfyN^ul`Wz|A9Yt2~A4u=DlroYJbD7 zUSMlx$({2`{#ZfqXHX|#B7-eo5tB}q$C}8RbCd%3_&f|Rt@^-^5&ISDhP<<`F19-T zD9}#Eqa&so5XVe(ob6VdF;j`+IJT0n(A5x=wh*dM6FMGgsXLC)#+2A9EtOXGHXXH| zy}l-X#s`z`w59_wD)z{R;v1S}r#h(UF@v^2IxS+pw`+9mBMxaY#d8nQ*T+u$RInQM z$b*3ncaAOcGvnxcSp}b<;L*xYl?1?J%%sFigwiyF@8pue z%S&3*FQv`ZD;r&9HGgZ-Fx96$9YYp!L_>%S4c;V*%d63mlTDk|j*=VCq!9k960kEa zX-={sv*0F9qOTlSkF?^AW~4q`!dIxzI2r36x=0<#j3GavKXtDpcsSwqsMWjM_oBQL zn@q}FUG0UVo#XfcM@lQt%!q$E3JwD890n&pG}JoorM@ft5Q%Zj za+n|_;(yh~aV=T<4b@fIKA2X`l+oXccO^AGe8#c3xNp>q`P!2m*sdcT_%yq)D3VDT z!g<&@2_i5j?Pkz5IyPes3!gz;8Cn4$QDvfdi11{^AYw#qhXZdYdV}jXZ*w(aN|n zMwg(SoVR?a^aMCWX9ANbv=w*nPiRxlX>X^E9t1+qI`fgx=<2&gqK&{4anc82b*=AGKYtSC*dkh z!0kjK?-nkxSFKm&L%R*AU^RW6Vnqa)T9@8=>gqH12RhUy;W8w(qGKp)gc#qSBv|ra zCG!6g&OztFh*xKx3lhCR2Qqeh>m)h;(iSL^&A5(AKu{GY{OPRH&!Fd@o-GF{o2$^o zo?>uH=1jPIQ8tCSU)`xb)9Hb~5ppX?CB=U(bPQ8OEG1Mzj$_93HSeUQjqF44$wyB{ zHB;8soy`{eYM91vC&R;kpQRxqrh4_mm_lxP^z+p7oms57y-EBliJesFhtuzwKG!cY z1rZ-&31x5D1!+?=ZN&EpsA&C|W+7bGgj-+9D6`Rl@5ascq8zO35X6H9hTU-?!5K+% zXA}B+;{%SqH;7#i`ooUfu8@_EnAXy}&L*Fj-Ux+Wc6=vA_kDOZvmEHA_EE ze&RvR#}vs*&r4MLm||kW>3hf-a$8aY4Qnl1Ix(f?ul=94DHtT4@S)-{9sX@NnAG?} z@4Xs(iUw)FVk?c?#nlymRLiEKxID4wO%+%e^gXcQJJ=rQ6$^(`9fT?PDJ5zM58lH{ zt8s2jKH8%Cj#rTC<>FDt#IZYxthRv;q1cXh?X9w`wiL-!%R3_c!hQ?yfqj>bN z90zRCqlZ7~!j!0Na4Sl>`f?_P3$%-GY)W#|K|D9p@Df$*mkR}8grdp#rnL0qv7*%6 zh^cps&JbZ+?nf_(c!QSx6H)sr-ILz$9?z^FK5;x_Bmho4BS6(rF(SiCit8M;T>7J| zwGd+{8V|zbeEI^cozofiQocZ2_0!-i$eYoe1#a#q6pm;xXpCOUr?xOHS~aYuFJk@< zpTk_vISY^3nfC0FOx^L&tXpYGj2B5CLt?uWF3$xiZ*UuN%UyvK(r3nxFvU<>RyGQ8 zEpc`6Oz2EKvqD_GUVhFAmz9Tx)hkRS-*c&0xpvY}eg*foz0(h}v?PJNtTC)+hCNVe#G#|c`AQyyXljso%NlaC=#m@f{Ht_|NL7A@N0inD06nsoq%Uy zE*ihV)(lg72a_s3JfUjeo06{V>o$8$wbdA&>+J5a7ea-vmQ_odQWcD)TQn2t8sVzG&ocxyBdN{7H%^eXQj}*w=tZ--(Bt-cE~cst-3+jN=%V1Ig~YT zeDA3Xb(37AkY{r$+ngAh8IdBd(Lq|DIxGo-x6bSXlK|V&&QQAQ!tH z?QoMmrSOJc2Fq-adh%`;#!2bg$+^X72W_mheO_(2PaI!u!kkl*hZrL!R^h_?gw2-N z#1L(kk@X%0p%}LpX%y)gjAZqT_U4bTS8pF?$QqTWYT)_Ae`S$)6P!wqv6+ywv&$Z;x5pz%^fqC#BMa|SW#a&y1ihuz(mNH^=#POq?N#6M78$w&kAJ#FBD&x2!#e=rlW1)2VARQ;gA>cuy z5W}pE2GdCEFtg6N@KGW=cJ-VvY=MIc_(3(W*nxW0YyN4JZU~| zob4pSt<)(BQh~S;Zi1hWOFxiNls$i$;=q6zkPt))n4Q*};Jr~$@r|7S6Ow-zc6=Wn z=BRVNq{yYSEmqdRYz|kro?|NN70EOAao3vzfRhEh9$jZf4Tq4X%9N@jF1sR7g9*cX z_R3DHW${imUwJSjQIU?o;5c1x4T4wB4?#`DGsWsuO9UV6H>AmTK-6@!^{K$&9`ZdK z4;+ILL-x#;LJ0P^)i{X=cl{#x$Jg75&+xlP=a+k_Fz)CYV3g2x^}w4ymim9;ue~MC z8h4HiMzEEF_F~4msKT>Q_z6+DN^bq6hWF$&K!FLCXkMx~W5tP>RI72Tl^urGXtdfM5wv7f9ELUq%<&>8}^z7Cn zFqshE9%mZg_X8)-jriPJe$d|Oo?%(j%0bHe&Wb_8jyH|5 zjlgpf=K-%)OTIEjOROp;7FANb)y8zLF2reMiHM!Z5l6zK%+=}2bAz!8$vJjST((vg zbA{k~s(tvcy=5`?UBhD)#3O~H4N%Hd(g z(I&lZHeg+DyqHnG!1#_X|J^*5*=}Sa5TnY32>qyNo#u`WH5ptg4?mV z_w~cf8UzLHz7f>_?(Ry^v}!bha=sUfQ+D(p1je5KLyyqVbHT?q?q89Z0}1|ZDt3Iv zH{ZzKj?MDBLG1CtrqI&&cb@$dC_bir*&b?}ayY!r*Kpj!iKaH4MaOoKVETP~1T_dI zHf%-TYy?jfZx`a~gBWa!MpJVdL-m*J7$P~BWTGyXxLTtzL*R3;h)x@!YjY{S^emF; zv^){o^HM8Qy?aWC4_rT8Sx`jPw%xCvK0NyImQK+^?fofGKF}viy|2j zx8n`Qd$bRJDjtb^M>Zo?I9HOhFLLM$uARZSTU0SH8|A>};hLU?&L^j=t4LEQX;T{g z6}WQGB^8@EF^)OcQdQJ(i=T7pw#aGbD~X!n@q$bV{Q&_nK7l%A$>Gapy{`K*K&QXu zpr~|O&J3sBi{D_5QIp8+K87N?oPgqgtBr_j3T72lZ)z`HO&QLiK&RW?dp)c#d?1K1 zWf!s%MjnPF|4wNcO|*4!u*jr?XUbe_iD|Jo^|{6Xn3C+QqETKV%g9_U-zA!gM7f$D zq?TTtf5ox24RmrrB=vX7^HMKRxOI>?+Ao!>#%v!m;8xW$?~3>?#0Nf701M7I?s2<& z6QXr~LeepMax=U;g%>-K33d$k?a9mP^iF#Fw2~?OYYi~ANl*7MD5jD^7uIaNs$yV2 zy!uh?MaSamuf2Ux7_yr><{M3Dx-d~lG>fg2D+5nYH*PfF6?F>qLew|2I4@W@R5&qy zr0=6LWBqI;&tvmRHc_M-72NmAG~(%sb8Pk{N@gjvXF#c8%G9*!O*P^~qtZMP-J_jx8re)*&?m>XXy_ z^7=&Rq$cVpksNQC(2rNJ1k0JhgZ94tmRbKbX?tMSVs&W$D@qB-#*8#??c{?bcq^ha z%lOKOk5l3@J#E8u*;*`Wb zCWL`-)dRq0lHad9z=Ac1M_vq$M5j9oa*E)ipM&xfvJ zrV`d=0VhiS`a|aAlo2UWlZrR%)pRYU^C6AoY`L`Us3Dok!%83L zO^fY=$9mFA_0zJ}xQw(rNP@i%9}xP~+zwM6^+h(iv(x_K?D_X@ug6H*jXmogPIzp4 zG1&s@?mx$iD~yj}6zol4H^ZVd<{!y>mT|L+B5CmALuDnARcXhQc-??wGg<0#wH=Ey zuBO^b43Al628wVoK8}y>V1Vfr104JF5nA>29yAWOn+e~N7+Y67FYFvnG&429E%DY~ z`$lg5n^ubtp5Pk;!tV65a4M@bM&7`MM-@Y5xkesx*;h|Jwfq@zKbEoCe{Gs>C_6|) zU)H7FQ$8qv;ff-?8el6*q+Wz-3Bs67@p=+W8oJiVGox%M@EfB%h>^r!|lE z_w<(jCScemU+8Gg&7q@{vhU#^nZIcc*!x>0AyG^L=^sA$8`O;@l)2L;4NBIjm4jgA|VpFs(G!(V8;j%kh8lc6SOxX`ZA})tDIt-o1 zv%G|*S6EsGx`Yee_O!smje+B~hUAN=M0g&WCYLGEU?cZ>uO88MkAxON#zaRsop7Sd zJ!^K%uy8OpDa%Iegm@&6@_4twkOmRl78ariHLkzNCG`LqO;HMm6yj>ged-L3mX$T_ z!DrBESv!LG!E;ErV2@+oVa=w-CM_D9*&}jk;IfXX=|(;=#phIVNn!Hgme;K*S@y== zWqd>0B0$s-*FM4wMy%9LShZjRiKnvc+A~-qPtctRnOJ9FsrEBypqucqi#Q9W!ETt$ z>$}$4Uxd03UAdKWIJBo}`nsmcjAP9Pxwz=iG!=}<3`@_eg_-C~1YSMi>8pNG@FE!7 zLA2RlJvXr>MXug7nXpV6J|OEENAZ)$D5=xBbFgtinH3>hcKGH{>j$Z3H$)Z#?V@F0 zQMlMM*IzZ^{6+kPq@}L%N7M7@7#m46r}8Q(!PwX|y1j~1Q-yU2DpxO;kY46KND&z4aWzx=xGTSVoz&!8NSFQKB*fNAUt3j} zx!*fritP+h3hS6EP2cjVS6uLc&1PNHD<@%GxHNQ1!5R(~TboiMdpTCTGwkE;G|Yr- z)sM;(ih~2%{(-0E&CD2v(@aG~k++1~$M+hBUPsG{Xcoys@$7-o+$Umiths3>dx9DptNM+?H*1+_Ux;)UKlYGkt znSC8hSUb{L-<9L2Gx6{w{(WDyxH^Z~Hod}PA^nyS0{=CnfFi+B?nF9`j zPSkw~Zq`OiL)ES3=FRr2oCaEX_d^5@f>qJ$76Th*qzcT`L+ATDEjDk@DK3XZIPs#R z!`N;}u9>t|)f4@@H3r(NsPNyg3>lYAt@JW4VX6yK2FG}U)?0TS1`evPp2S}H0s9u) zK^69O@i@s^G4;`@>~6Vt`2l5cY~fNyb|%9mCOR!Iv-(Met*Fyhkgbm^(;A5irV8q) zPp5aY{oa>sX{>#fihFv+T7NoaXMvdRS!3<&~fI`Oppc8?TRW z7~}gD22)lX%5og37JupkIBL_&&tcKZe7oCRT3Slyd>ZR5Tw=;t1g6hSR}sa~T<$b= z>nB^(Fj)f5K)3)`xigy!CN15}NwZ2s%Hx_c6B6_;Yp!ZE(^~5_K}dEIonEkbHzu6z zUtf6wchQaoai^wt!zLI!?Hg0lJr@zQgx|-&2h%jxg>|V#MUCCg@UqDgE=w! z;(`_Ve6!9}!pJHcMZ5!)Jehy*|LUaAy)X+-!qFYaW76tnjS#&3+^94C~Rq>3L z6Gz&8mOxrjLM&x4nq!!Y?L?L}G1j@*Wm`0h*htLHlOupZN66joI$2b_WqwjG zbtPym>DrVQ4tiM3#b-gmoq>>(ql(zg4|aG5zF6yy_qy5D9uMz?YZRV?voXgEI&Ait zYY5aduhPiN*^h242kiohKGr0iK#!Ft+BbCDam-EOs7zf98=^yZ@*b&kslP(Do+&j= zo1R>kXGwG=!kuvNY~oL&Ljnq5pR_zzU*8??k!{yqC#E1NF-AWhd&<upu%$Y3-^UEp=*+7< z?0T@+#K*3@@+sM3v2u3rm`*V=>m1rfqTNkL!ioxQ93S+Hchi}&H|^sSax)l$^G=&d zw02?_Q3&aXXjhK*$<5)1Jr71zY26y*TW~c6lA{=wWH`Yc2G1o@B{N3u9oKQO_Z+#O zlvLqgQCghirq<0q5+ZUJK?OIO%ryend;1GQ^YdQlZ4CMdp|$HnUc7S{+jDP75Zz5H z=!`95#F$-Sbp2e^yf|DH!&Kjt%|PA9$esT{WDKboCiAwl{eIaVQvQ8y`gVQNuxuIE ziz30_6A(@jxAO`7>+)^D)@!07^PPN|n_Q!4@@pY2n*D2$bztRb-k)WTh90?)J_7>T8|7Vb!C`osLTArMow*2g5i)Ob7QA3_%IUmbR| zIm_~`y>?v!)M2vQv^~`x!JE-;9DF+WY1Qw4jClQj*FMU=Cuz$fkugM?7k6}m78p{1 zEvfA-2&ojWy~mFX;7JM0&yfPI<49#PfLLQ$xS_(_{0?+))kG2K{iLfSJ2ojZEfI3X zHP{-NKoOlAO>op&u1H+Jq0uvdVhXjcP3x0Ho^I+z#GcC0JYKk~PUQp+70RXxG`tfj zNalLiU5jn>d2SSA7!zTGWctcYO+)zTT44PMF9}UgIp=<~E^OeHU(f+7e!2wPHKG1w z()iX!l&bnFg9VEa7_9tR-T=@@ayPUQe-|sVDlrFK~x<+ zgTo<@U0_A`t(Fp4r79GNvHy9fl?IQ$xXvvg=(d)2q+yoVr$;j&TdLjm zV2mj=NzpcROPod zkE^FWmI+e`VTRNg8?zPKTKS1NVivq@5ll|eOCGeoR+XrK!E2C@kmfZL%S~l`y*^cq zfH*?k_lpETSTdKRoSl@8iJhN4Lzc|g{Lui<){l^huCY98F8$&o=#-7fL7an<=mRP; zL|9#-o_ww}Jr6kdI&2-7!gQLaxw*k-*<^4=O;$>57CB}O+x)TRb(F!b!O2Kn6=63I zMI28UF*A)UCIEKSk$!Uiqs_9{bbEwh+x-W$%+fk%^WJ*qiPXY-VI&h{Q#IoBh=q?UF|Ss}p~x2$^!H5* zkh|MV@!XaD&Kfo%&vrw0KhPT3RQPD%VdEk(mle&2?_5IzuKAG6gs2 zbp%87GiD547?yRCr(XK$vGLlWSbsx({+zOXW1CTJ3aECD14d~k+|Iyb3Q44Aba9F) zCtz9~jgjK~l*lVO@4erTxn2A~O#=@g7g_dUT^$biJ~^)XVLb{VxgkUdYAd*gmg*>; z4cZ*9neBCz8t27LLAna1Y*p*3+fhavJHxp!bsy~WgA>f0M^LXck9cH(R1t{WEHrG&Z7iOLyo_8j!vPTc)c zgYpya|8-Smn@JMXPaVQeTzKY?sd`SlY|NydM&O6{pJW0@9WSe}4h4ohG(7dgRelj; zpQEq$!?wO8j;tDcSdD64+nqdQ#d-MX@OBY*&>u3dE4u8~tf|dBd{&g_P?qv9Z&@va z!Rcj2hfy_8W9nC=LUS9ejlRjB+;j3XXuG7pBs_QVT*|JN_qo%xzdzhA0|))Pa-*Xo z%+lJzOdQ@{v)iWi#x|iRquakk0p~y+vq!q90vR7+CA7YnfTZ72JyggZQ(f9pB;%4s z5k-HaJ{!S^VQ*FsH2GVSgfvyZr5NP4`GwRo+jK?-?fbW9iXS@tEnUL$rJF~HU5KG+ zV*`|TKlOE|pd|hqxgzr*Rj+YEn|dHmrK*=_#r5KTxh+uDz?r#i1lLcq#Ss7d<+ zv2x$!M+WKtCtBf;Eyz{-i_o?h?MRPCQ+D9@VsdRIzOj6~NeXm#xlnDPs&A!BV&V@h z_@g9({tcNwc(!Vf5iYEsJlX~t4}qX`H!?01%Q1=tx7GFOXHZYeavk6r^hwG1G3*E4 zatR;c2KKwuF^y+fjmi)y_9+ATFF)$%atr+-SHga~*bOV~{D&lAP-@P0;^20TyWfTW z@2`4}kMEbXIJJLt8RHMzd_pKTwO5=Qb8de#a3e>qt^!}Y=Cq(VCo6}oKKf}{wX$af z)|ZnRbr9XNr!ZxqFexOluW7bPn2H##WE2L|bj?wr1#UPSo5qXDn-U+c^DPOTi;hws zs2L9ZEg={(qme*8iNBkwQ&%Oeq^P|7^5^XNM@3!Ov+{CVhYG9CvTU)HWW1Id6=&cQ zY02KYYa4iAa6Q+QNc)w#cA4TH`g2LE!K}k-l=LLTF;CLoYA{YWs0gUTAEQgYrMmvYcoF!k%-v zxM=iqd-37g9(Z?>iH3f!aGew{9d8Zw*uh)CqvAR!?_k0S8e@B&@WcajSe^$JT#E3zZ|X)lFa zj->~s%^)ZS(+H0dOaoI>WG(J+U6SyN!^jhb`0mVboWtNSjZfDvdP8br&>_SezXAq4 zrewsR?8jn#1+#&}*iT$KY5EydlyDl$EyBB+Uy^z61pdQHCOo>+kz$$zi`F`=z}CP2 zD(z{R>GR0`IQzpgGDK^+U)8nT*>mou_Wg zFP9tp{Pv9=AJWhx-zne%OBXbZO)_U#2krJNctcnsAlJ}iQhZpw}kTrT-wVQ!d;x=gilbB!N>pwfwfDmd}M zCnU~3kGs|HGG9*6q@>8}fg8gFzAPy`hCD*eq1Wp|ju1|2zAMnFMrEK{^P%8hi>h99xv03)N%Bflj~2qt!Lm3jz0tne}*~vUUm+?8FZD z(M2ou8eVCSpvhh!+RK z>SAF^?7GWvJ9`t`#}kRngj`9zUXwXpcToD~(7~763L=QO9RK2J zTA5Ho{}3?XiVtyKC)#9`0=G!Y>+RVAXtCd~)#w@uXu;Mj{ObZ)qola0=DJ zErOF0h;V6FXn5E|Kx^ETM3WxxXm4d(c~T`Mzr+JJm@jlK7N(egbKXtDLHvuK^st``iP1 z03Go<=mg;^vPZ`Vb43xuiHR(H%F18tKeajW@Y2t%X|i!OJ#Cw!$Hfog@pvq~Fk!r} zDdyha66P)s3gElGJ(Wf0zEpqAx5bS;AUQgjPl5BEglHkKOR+{bV(eRge*JY4zFT)I zPqkeXh=C3CkDoz51~xrOc$xMa=x^^O{==F3p9*|C2)gDIuYI#QOvOIE=yaBA zPgp|Il@BdighQtKrLCCkcUyZarus0&Z~e7V(GjC5!xTXn0s+%H+d-{*aY|jd@3N_y zUTNg7soFOU29yvV9U)||h4=>pwVtrmIpsc)^tJ1LK7MNO!O{2wWe?*(;0RC&151Zi?$_LX>qbCOgrEj=!?Jbug^b!NQ4gecJYqxf84F*0{Eh7 z5_mjjRVY-H|Bg0XS>I_++%ziX`{Umw!q+qHrPx655(k^Nx^;d(7ZNVS+k@jjd{d`{ z?|M!x%sO>m@+TrI4TxI37UB7AcIrO@`3DOACC0zX{t?n1f_J4EUQUEbemM~&FuP3| zA*&p`n&WaK3@-1U&z!ffPnNwvTnW6;>2E5Jot)HX7Bda0bsnp>`Z+PnY0;KRS3bhO zCA0K#&AHd*)IZW%ng%#m*a6InvjGCu?0t>zZp;V5Kna_8q8BU%|55fit@L>Ioyu=P z?9sD--)ubT*C-$c#VLT@tE@%7&6fB_Apd;?waLcgveNDUKs07=^?|X-hZTAEQJpca zaoXdTzs(*F{dd0OKOpFToCSY=9j&XpX~}&l;(xM)m%>e4ePa6GJ}LdhvHF#CUPnB=R{tU4c}-2BrfMSB?1NFblTfR9UG)3?G%^zIFP`{tc`ISk zl*#9@Pl?-Iu1b0suhE^TIODJVwq8a{E9=6?ENdfy-l*1H3NGrP@BT6|-_5=4Bjn6i zrw$NpuJ`{Arw%xhmKIA_K=+XD)?P(=k8aJ?dRqys`|jl9v$P>mtIVt2_l|xL9;97^ zt=N9atu+iN+&uSA;W;wMj+5-5x_{%K$%pp$QMm(q6dq_~ZQxcl-{4z{l7j<6SHTrR za<)ANKxRj>0~)C7&)a`cC}x)74V2L>q66@&xSjSUYJK1+pL-`KzFic6vm2SVfloU6 z;cHQ+`w2hOPysAm0M|}Yh37iY+UZxjX~6;E1%CqFeb5xP4M^T?8v{`7dZz7mq0r`? z+ON@ea_`2r(Y$Tk#$U#LcW))R{0lbWo!Sc5b%MI`w}C!yj~>ASI6^1Exk4XM+feUP zx2yN7ogxKj`8`GtIi>pwmZWt(qP`86XY{ij+RsV`LiDWmSnUg4eM@-o}t9Knw- zzu6%rFK(MhEp zM0DTwYYad6?(PnNH`+z?&!F0O3cM)^Tsv@f$~uCtf30;_bcY-SSMCn;g1=Iur}H(n zQ}OSC5Zk7<7j^&cPU;I9PPoU)j-vdAW>uqrBE1P@Km#p%4UhJzJ3_p4BS7O8aO5m$Zz1`tX8TXw7@~G{W zOW2W>9g+M4oNb9j??~jtpC!_wd52qH1v(}D8UAg{Hov}Vi97GVK}+7F?PRBS)N@D2 z08K6t^4L+&KWlRDLHJ*3vg2ODwkgQt#Vf3QF&vue$tOt6=3Pr zgz)3IYoL^W*opmd?+wFg62_V$mh>Z?OM3#i+zP@s?sQ*vtgwHrC7*~#a+{VWOl4tbmX@}X&R*D591i?UZ(L$dVKsWhMXzjbE+yhmjNYB&|2xCf5 zn&cBtHY>$?H8`5T>n|ix3KF}s5-aeCnE3EMiKgdS)~dhC?lXoJhJ4D7Ov$t~3gJfT z-zWCV^k_-*SLl&z>V+!0rKE)BFBUouJY~XVCWlARq^LM4s)}`yMEP0-fs( z(-d##TAOP?=t+>Z^j(*QlC?)#=WZ!)k1GxX# z#SQv0DR7fXh2piPp*FUQ-j|^{ZJ*kGb5Nsu z_zR-Ri;$|{QmHsQ`%h3mNReSPvk4wa#MOicdm6t$qp(fDVd4UZ1l`!}|bs)$zRSE8riFBn4C9L#P%XAte z3d&*4&SghA@A(IWc`76Bo#p+Y2OOu3h7HE;>3hkOQXZX5bbX-lE->BE!4B`aJZC@9 z-A)xWco0DXMtDe`Lo(5Q5&k9K{ZYE=dOfE7s8zUZ27N~RTF_`FxAa^H zcEU=nQTvS{E77=S4JS0MqnOiSCR z)~%b}_|kRDGS!A-zQ(iMyBbme9|yxh00JNZ0%-+=1!=$A1L)2deFSzHc3lI0|NhwX zYu}v^y`?y@bh%Ocpc8jqpX33>c{84fAwZ39rG@67?UQs99C`PBc;lencY8o7`v+u( zjkAvI_O{!bACyw3tbFA(=)0gD9p?rG0Wd+k-tX4IF3{?tN!}fUElOJLbmcP^nd^9IiSG)9~N)_2pFJUyKeG=jsO-21f)QR z2EP}6FlZeF*nqh+enCe-pb!vp5HKg8BSqEPAGOymL962C;ui1Z&5EbL-qrdW0O{{P z|J0-KyBHHw_a;As&O6ANeUk1I9sM;Nf}PtN-*nU{rtU)HiggND2BPQ{M$`^CD$GAX}gsgN_o4 zKx>%aqr#&O4`?S|-HtZn{!Tyul=?vAhgU%Y8ELnHQb)XaclZu48fb($kQeBT4nBg< z%S3bIrHJ9utGz#gzCSqltNh~c65xoK@!>s#*OK(F0c{Q7JCbt$&>-;1l{hFz21I>u zr%&L%1_GU)+g-9t`1v)U1-*6wJ<92AREf`k4ykzC*z-71H>jIWvi!8B3PJ8)NcsEE zu5928I^cUwCUS5W$rZ;FPxLfk4$|CASbM~O$#RL=khX5fr$a?qnF3gLx11-Y4}v78 zo?l3k2H$?yu6+~!p+(a%!l3U*i5a=YA@TF?S67AZFa~-H9KdbE$uoOI(VibOKg#<{Hb5jI8xl3q?qsx0-OznL-@Q^5+aj{lmqJaF%I2NwF6h0>L$-u=!z~#_XX`9}k!~)j`T( z#?&bG5eahCWwq&?b1HHQp<~eTib$yftFQQaMN8PyU`8HJY!z{hPEt)OW%k?>BIYfu zsezrf)_a;h;`yQH<_8}oFRvRn&F%ckScMO3Rl1r5d|V<}5NGV0cpS4Spj{RhurJVu zDh*LH5aDSwwZ|4Xq*H`QV*1ob??#?L)tQ zZwl}X_Gc;kFSPc@rDByp`z{1+EBzPy4p?>&pykTkM*x-F)=Da%DAx$e+4qS+=5Za; z`@tWbOoON((91X9{d^6ixLfqa!mH~FiHL_+><&AQ=H}r!n+x^n%ui^HIC=^8NB-O^!XL#>pGVVsNoPOutz} z*iH4XMSGd8r{MgcTc_&hxQ#X2>Ho6u3gYp3oE z$l4-m_XLCT5L%PS;J#GpTN%aJJAGmLqJ-PF_5gQC^mj|fz(s$beq*ZKce~xjy2zaD z#x6e=o;KsWYGHP$e!`fC;@02y`W_w|>fwc@N_=(EQtJ4u)b^}_{RWl7-%g0YW9#zj z2o5qs;bc6sFO|>4k4j*3_K9}I>@iwCFt5Iafm3`P>gDV&-D99wkIS zSe!LH<^gsxt>4%|n_8{>bQ3c%8l&<^Xz%GZs!3u%bBLq~+ZzXQb!_}~)(S4qiDwDN z$DNjX_Qy25joz%MDQS#qV1JaAaAK4eBih^3dDgyOFMRwoshpNZmg=0YZXPzX==O9e z*d^;BT=Xo*b13k1P-5x0E)BC)x)Pt6T12PpAJ zls_yP7r`Iy44-vSbLcNE&Cir237VuGU&$=TXN-D-eJHC*2X*YGG8Wq#sFGw#W_zXs zZ|UiqR8Qoy20MM$vS+o&hWBYc^u~F=8Y2#N=FET{!m$TRbmYtl?oPD}N8D=_T7Na{ ze-hb_WbJ<@Y;3dN@InTVj0~q8??c`$NYFk!-5k8iy(|Q zKphw}>GBkWr0cxk2Ll6|o6#_%w&a+2dEo~J0}NOsO3!y(P{&Bh{H(Dc11KU|g$Bnh z#egzHKtO@9iF-|2QM60qe10OY<|k$d0m?9Z&qa`iARyMTqm!W^MnDNUk8BctmI!un zmn`36a{{%I-KDyyJe=tFNC)j~vhBuVRFjF78BD3w{j zGqmf_k(;-{H~&rgw~x3#8HiW0qiIADdfkBtNa&|t`SI3Q7;C=EiXw-lKM00dgFckRMNXMtD++cROFk3El`V3T9; zL}UFCiQt_X5y9XU7?mRdOl%~;;6XGQiGr;r*Dpob-c&FHa6#g=N=;LaQn$+Ydw02< z%;~9a!XsPf!-=oA66R$S-0iXR&Yk-Z{sznoB)$F}#99Yq9-*8T>%f16UmRI4)NUlt zAs?n?)R<}iS~->cuw60jLGvy3;~R?^9#iwfeI0%(F0Csts7GYJ>&#fu;g}n1e=GKy zY@HZjmaXTNiNRy`UT%mC6mkodk<#%=*rC>7IUo_$7*^-?Nmc1{%iXcvRHG=B9+koz z`TSFSiDw+Wqj@n$qGNYgJpWkYZT-=PhP93;56JVAwuvUkI@O`&a@_ksO2S)-%OHSKvdhVYbVL>$W>YfRgSlR_q&)H>L+t+KKWry`M818?JuG;_2hkH(^ib% zCfQS>la9P}A1azbZ;%3jwVwWj*S^bV>DJ8kMZFGSYk+Aiura)bMB>aq%Rpo)uUeyn z42or$M?>rufgD*~*x{rdk^W-4Tm|87u`npdjNn!csbXh@1N}D*F+@pU@gE~605XhB zYhFdvY4)BF1k@NP-hi6c1@uI|8a*PMSF_*SyysL(VQ0_iL}&Bf_fG|h5F!M$2B06{ z&ADsf4Vc9MAxF>!V6Z$r5@Ln8Aw@s2EGZB39_1Wz;v3(grx1{LfJ6hNMF`jm=wb}O z8X)k1-#{FK1nPZUp4QpLoj~|+8Zv{pIi0G=L;;!ky%J~$;-Sp~!TM+90*J<{_s|TW zE6cIwlh3zu?|FQVIJe$3v@YYUYnuO>Suy``ug;|7y|F?oKC6h+r0uWrOTp4)%@-u& z7kn@1d+~3)YxiSoar;t!$n%-*M1heIROQ^X(S?)bZv3dcn!Vz!yJ&{V7j2fVcvm64 zL`=Ti(E^F-GO5c^^(OLPFjai&pzRF-cOA8+ndMK z=F3V+8OwB7M8}y58qX^A3`Pi?43J0Trurov-d}!_g(b0CkkoV#Df~T#x-ttR?;dX} z=7;Fr29L_0TE3j?JI9A@A>yCNt2ZSn5mmqjMrYG2Ym6DJ&&qi|2oOk!-DCuowMSp0 z&Cv{iivtqAeFNxgb$7qLx3aUlCR+M>wj5-K7>>ta*3Yx+RxanK{zh@1ICQBZ#S& z`XFB1OI)UE;>Rf@BhkRnM9Lgro0DErMS(#Pz%Tg!4wbC%ugj3x0U~k-+8;tIVVVbQ zTFZQ+N=T}PrtTiq64oN{!e)Y|1rxRlST3xeRoj2*5Qg zg@eX{9i85B#5*4!1Oa%|F}j2n7`>qI{8|zkV5DShH^3_*cybWH2nf3V`gpON#E%U_ z%sUJiZu{KH+q}SVdk!E9D7$I64_dQ_++O4Vdk?*j>TrRko`T#zPQP``w3N0B3$M7* z^mu($Zp7FgQ+F0n&*~p+j}NgU@v+@gj2^p#mwkO%$h{%Kj^LiwdwtXBWQk+C(v`3W z8LyT60-8#_NKYgU_4XumVxOcX`QJO`DK0(rcoDb7bXxe05?Pad>9vP)=Pqks%|Yl1As|Za#j{jAW ze{{L-n4!~F*p7S0_rXg@UCEzL=hig%WFQ*?$_9*-$q}kC|1vp7ZI8d z7H_d-xFHq@CD+@Ei#peP?_rFxx}Xd3zbyuJu%IMtm7_x>08UtH00SQcH>gJ3AQpUz z^U{@vQYRiH8R(>jvW7z34CgM$yowV$SdLJ+g>IOJ*b-mq+O=*P*;-6;Vg`r?1QkH>q3|?DXT^r0D&+abf&az-d2O>9$06oD>tKk;+u=_N!l4GWI#P;&w(c&uiQY;4Rfnx@ zA?z*cS3xpuZ`Mh>54?xUEtge(RpcLCuG^ML>DhNFp8;LnGhP}@?JbL zs^y;%5c3Y$WB_0maM>UP>j2m*2a8AsA~x8|TiQiuqVkEgZFgoy3av6l_^H{h;i_8s z_)CBY8G50%j~H--JR*P!n?^!Gql`hk-h|+^)UkF?jX~_nH3-i0Ma0T0N&u~0MO^a1rvP9(1!*`?sPeIF()pk zRs?ne$tWHSBzAvDA@fT3-3?nsc1W{!+Q*IohQM`9;)+8dR_solH3$ESN8h1IdFgAR zcu%w$1DZ#x5X%62KVfVNkN+Hc5Lj3Qb653Fz4$7oBE}7#0!!`Sv&hTwb z-~oUJ*M*Xaus+5F>&HN;gL@U+gb%DtVCoDiyf_)QOmaGj*RbeGFN7Q_crIS!MR*q0 zaL6TSR9b5S^|WDo@QsmHV48xhr@-0>L<829)T7COK7A1S*c9Gf$t}zc7$=c#5&O3| z6awp!9-c1K*?w-gH2jQHwZfS?|F3~In*=_`WjY)w7d(%(l17PR?ykhqKv24o4VR-Y z`N>LtY&hj2HhiYp+&4P%$?tf%&Nxs}T=+xeGc-lT>sckXh?AtP(+*<3Mp6%NrM6+skF zJG@Rg&K)pOa=Dwch$>;?abe*#D(+#XW_FXyvu3rN1^y1ZNAWsCKKMz8f%b+N;m|p3 zb~b7uIyNV1)iFh2(SvsOutP_#bxxD$q#JpRL`2@rY^b=OI4aJP1ba@a$0-6OYFo2=2 zS?)n23{OHN=7m48a8xG8SCs+#IXwX|1~>r{fJ8L27`Qlw%|W@kOOSKL1;!UVgZ47b z(SsWR$PyvQQI7Q~=>%vj%v9a};8ibcX*V$lU+BGv3aC{x8 zDggzyAec6mL{cS13c}NpUcVEP@+RxHr#KW~=9H!}Za5=$SKe@EWKo@Bbfo9&#vgh& z#xA`leK6>+8#n3dvP^W*ZQ${|>@Fkz*!jx4OWrRIXv;0RRZxXPG@=K}qK{KHGIiTc zCv7#!DP=bFByYTau0eadGi3*PPMje0E;2nW!?>fH;BD?>WThps5V?3UT16;8S)ANo zw}aL4>!$(VJi$q8BU>42)=H|`GTf=()1Qd;S@-|L_D`5x?==1KH1G!>GcaTFHmp^N ziXC4808Up>)>LR%05t4_5FPG^ZNd|dsm(mAnT*&Q;rCt@nnM9HYzB;H(1CfLRUq9- zX-)cUSH&P%!|Yx=!M4;jLJgT8_$~>+B%2AY!|Ccaqn&iGNTmpPE`FC_-x1_6bc_vS z5f`J%w>1Q;Zt*De8zlRM1x+t1>4iwFg0F%RL^?xef}q`%9E?G7tN>ZeAT&&QA-x5l z^@!F0eo2H81J>X-R<&316wCvt-R+Lv(FYy`)iy}E%F<588@;al1|gq9V$eFk?r;a8 zQ2(kU-)ao6@@9#od8hLgHipgI3fDkU=`jA1q`gTxbcrIQBzewcX0g0hU0vshxU!(+ zBeJ&}rSLo)TW-7$6(^4Ol-feRpPp&DpsTxkl>_XPcfsk4<^Axbitt?Pfq^vV7~{Hx ze&aI|-4!&AxK`U$jWP%CqV4H=Qi|rb)rV})sw+i=-EVh|Yge&;K}hcn$~83BEq`U0 zvIT4OE=kw;uy^V}14Sc0qa93x^(EiSMCE5bJ5FKuu9MO_Zn`nV5o1pK2E{3n_0lp< z(Nc^vrEY|u_C7s-olU#v-E~vLPhTQ}+i?5F`wqGEO5^6_Hu)m;xN7qkO1%2QEOe%N zDzSAv7yC8#D=U4~TO9p(0*J7?C~_XY7bV|%$|=#_B!W#Bz2kDR!SM~DX0E7ONzo5> z9~aO1$97FktKr?wy-_vQYop;pm~}$5o!Km642$)hKR96#Bk`jVnu`~%x}_@lrG^$h zEJ`D*TDf)@Z6S1duP*yLCy#xDQhPS;|0JxR+b|&gm9hISTO)@?sYUbyl0Yej@ zhGi?jx8_i22+cO`wfXYyi|oj!+~tn}p5Iu{qzhpP5Gw~`CqUfw1e)V!&?i8MpYMPS z;07>*Q{=0wvV$1B(4eXbFdxu`T9Pw} z)LAWL6e}{alK9@^#KSA~(V4~w1u#u)L)G{hv^B2w=8-SU^j``P?-q88$9TocR)( zWYGPlT782UgxDgjCOAk$Dwb6k>k1J=IRrUe8DVeh*=Do9Ks4j2p}Lp$)N7K zXaKIDRG_2Nm7AXric~D0a-lc2D=MsJ=o9Bt*cSm3s1Fg@4P|O`3X*&`dhts07|D(% zMhnTylKt2T(K9tzLPnx`abd^M*TJKzD-aqf23MPdG_S!Nfv4Byx8t;5=BtkicMFdFYi`@?{-S-SXV*%%?H`XINUUa z4IA>^-QRR7XJL3RM@62g6R#0zJX>#ZGxlMd|LK`B801IdX<&PRg79Od9C3=lytc!< zed2q^l(Kb%Oqz$=6OD|nS@c*~geDaT`R(bXO5AOU(5ml9I(?;0Dc{l~QKP`kvLw4O zTZ5)DkaIk;o{21wR#Bg7=(?sT!Gj8?Ke3ks%eti?ANUL^eJ@7tUeZ4bO|*h2-tXuF z;3fz*-$N{=orR@$waD4Bo-qrb8H$RzllA7eH13XSs0zg|UAh_ZKIz>qkx^+z-HRa# zJq;48j*LcZ+oC<_!rj{n+p6{d$;V^{efr%nSS0P28zyb-K76RJ@ zEJ*+MK-b7&{AC5i5eu`H%JJC=MAZ{{PGxhfd*vKA8g$Qc=ar#y03ZXMTA>Pyfz)qPv)T3$#Xg`(o zZQ@oDPOKWcs^ta)tyMV1g-%ytZki zcX#CVhP>;KJ`00y<3KO|i+SeRyf*COAMm*Eqq8@%bvJax1Q1Ai!S^9n1~A5T9)MUbMCy$(GBXA-0TU3k zZrgmit~`P0Lw5++NdBwzOAu+qmf~SKwkY;4U<8zkp$hqqmN%HP`QMesQ&7uVH;VMa_aLP-11vqds~f(LjNu8BmLi?%4ZWTGE}LnFU@^o zkQ%MwQp)f2_H-LAIde$1;OERu_3=3Cd{kRB{;_3=UwUR8cRV3Dy0>mxc2;ED=wKn4 z<6M9*e%93@G_>2jOfI(N+PK`6k`57SWr|1U<42nKI=|$8mEprJR-@N9ZTH!>RaC1$ z^5PNx0xGiptM(%$XDRtPrSymtf;(B<%^&Mq`2N--cSF4dYPl=r>$W5xtG80V<+QFu zHd~jo4KrlwM$w%;hr(i=7CjFAH2%uvQ{3L@ScF`cR7~Rdxt@jmqKr&z`Wp%}>E-An zwxY7OeFkRZqPRGn#vgm99=VYGp4PJS*w9l%Zh%ctMX5k7{Fu6xxFrVF-tGf~4RREWyAe8QM&|&NbwCk)K(hMFCQL z4`?2O?%LfjK!kw!2zB`DsH!ZIQIvHK zXKgo#>8JwXXf)SfDA4xdK$$D8%6EfqKttSsqt1k~nu5l;tAs2+$4xSAnos3?Hy-0s z%GLvJ5ZAtHj&tl&8-x5B1f#!xitnta0wF>3eK*upLjHZ3vz@-&$E zY#Q1COF9g~)u?UrNT#_BeC2mGM)=bG5d>GA?kl+zYjp zF&JWoSeeC`8GAqkw*1($zIEItya95MJN)=RPQSP<)K1**39KiCrwCpUzZu@9=&_g8 zB1GHW08F;E9d?vYn|oQ4J)@#9^C^Cit>EdV@^Fm`vg4-QOfbh2?f3dv*RrsH-+8?c zs9&-&2AS0vUOc#J(|HQc9-@kAya)A3aLqu(*hYss(Ie(?wr5ZRarS%jqy{2NfVG~fvi?sm1gt_y1ll?+jq}b$bVzoCzkA7x>F&) z%Wqv&&;jmuPUJ=}c32z+c6LBa*;zAyyB`AH(+xsimcs)2YDOHgMI+CNk%6iL^%{eY zGAA4_pr3Nnxd*HnLE9$o7>XCmu`(WVAAh@qgy=Wdc+9%h0_Y*iHXugo-w-k;MtEUBK=n|FDTY-%m=xP{;(6xoPA^WG;aQ{=8L z27lnhP)I~hltHu7?5we+xuMZmSD@p{=Sx%6sKfr(_Ou!6p0K>~u*#-s8r3K-sNpMX zs9ZKv{GewaORtf#evm9V%E&U{QG~28LEK_v-dTLXPfc#*_kMecdme?){4oA(+=gl` zD;YK2$s3_S=<9WBI>uYCoNzMZe2d1%uI@n7RI;%i+tD_MW@YtIi>B80fZmqm>l+%2 z@_e(Lwk7!QG~~R-fB>?JLd>DM98}q?p-P1f>1Fd*j-PG=IR`LJPHV!u-FII_!|V^% zN`S7Dt2aVqvKxD45UWnyx@L5TKv*!=pHaO05->d)e;C=ya084BoCJQXKVZW(Bwp28 zDOh38#R1HVFBornq0As6BwEYRr%Bv2vW86G`Q(2em%s*P+b%>s%El<~(Ag7_<~L&# z?IT3^>_>UOplWMx9Umi{wI}3^?4r?F+;rGC=<)#4Z^Kpnfk5M~FWM)%gtB=0o3ir0 zK~K^iMGVnj`ZtF^V|_jhf|Iw80cYA8+)Ji%L&tPvX9L{cA%U_z2dHu~F^C8r2WB@g z-ax`|zu~T!6J&}^)1^DsU^IXrUM(CFMhS&<>YXm@f3F9>W_DOkkBbhA0$ zm=HFtvS~=Ux7sqpA_NnWHi}3X6xckBqd&R3j7!bGvC?}SWr;N|@N4E=@3IGw4N5Ti? zSVBHLlbW)J?75{N|4k!Tf#56#B&Xllo8`1g$s1fi6aPi}^-R6|Bdz`_R#UnZsT*F# z@0|GO__EBsin1~d%hYGaDJ|DaiWzFYBnsKF*|s=gBfA=LJqyjaTP9yVPQP8_0>6FM zpA1_3?R2dj5DQ&8g&CATGw`ON51_&0;OUV&_;x?MR(wYR;>k(9M0bsW41h{oo*i_U zj1psV-|2brgynsP^KEbqhmm!A0o{ZHZoml)@h`$IxI)wK(hnehpbwY=7*{sIQtw(~ zGz=-e{V|kzU6zH!5Zv?(re~N?_(JCfr+wVBW-zUEu1;k%xe+sB)Z^+x&!Jn7tme%H`b<9?WuQkfquK>HT`M7 zKm6Lgc~8232Z!rYhTbMxt$lGvcxuwteeX$rzGN~Y8`n+3Ul5Wq^TrcmCxdSpId8gI z{{_|dMN|Fsd1>mzH|VXuTDyueUh{Kf+|BGCW=^F(K-Ee6ij-EKD2mI_FTgu&weHOB zXr>rs%FsSI^G9`*%In#LaLmpiJS6hih6vmyQTQ?#j?ae z`FYLcQFksynsfsJOLjnHIAiZA3@q={oL}MHId-e32cNjz-}g<1eYMqcmhJ&6f8Xv= zrH4hsv=5*6aS3Y6p2&ESHA_ompXo^}aF`+0BIgGy3_T{DC$++gLbh2u*p=_{!;&at zS?bLTmQ;!}s(bPD>E-27KV|hvx02)3 zeX6Ss>P|heU#JESc2l2)R&K@cNxO3UNg1avACI-G9%xayCFRR^SeHxwc;7-uVT8kl z`g*)D2Wskql^G$i9O-c2Ra;nGSJoR8MjET`KC7~DwerHA>v64Hd-M(P?og+^Y|ObV zKZQ9-3mi|ZGk?tP2hV%Y*pDd6e)@W=1E={z9dEmh@042|HrT1c!o1Eab|w6tD6*ft zL`u3DJn86z8Bqv$=sTWh6Fz)5;i4J#7HU>14*NBVlz=x+!F0X2Qh=vqEEoK-S zEO$V2^j)m!$N`K1kmzGH5SO{l0Nyf14*hul`NNWGJQ310BWIlqgbVR0zh50!H-JR|9b?4`$PO1cp@b9D;daOG!AV)rBz@LL$qOj?zxJba-( zz^XMhr7g&jtr>b+X$P)ZBU+qw=Jn{6`;q;RD;wq0T)u2fUGf$2Js(n@`X_PFVwoH6Tq+1Bq`s+{G(*8z3hBz~UqyVHZ*lY#! zMp%0RMyenp%x5>B<&{2T&zki98p;4n%O6VSMCq9uV?5joJMc1cBzUeYYsMN25F8A; zUCj;`lU+4{z1N`-NXP4<<4(d( zB7&76JRv$I8_{ubucyGFb6-Etv!F@ub|$a+1?VILmKB&zv2%LwVL{jL5S_LUcV+e0y5 zCXJAiyC;il@MT;>?KAva8pZn1ZVC5Us$8sCI=O~gXPC{n#+CHaZ3{K4o<7)d9LS1L zGMLK;Ja6EzYq)r4kYprK%{c8Q>^ya4QCy-v+G#*1#gmuf;#qBXWiT=x#v#BO(|=@FqNAR8yC}NH;qH z``6Dwx%FXt(y)e=yB_32cu@bpNWa)S{s%UZ{=GF}3$S(VfjgW2QgpW(h#i2D@|RHr ziV2KPK=OuT{$S~we%32#Gh5!Zii;7{77y3aXCXOB6? zbk8LI=yL`IOCli3YfoP5b@&bZdFCdNVn6xbDh7{cOa^ZNre=8#oq*~u@8LQDLCQDa zTSVc-Qh;(S063sWAkh02?%VZ$dLB41$_3pn?|lkpRD&n!5ajkgDX;nw5azYCf!*?bz&A)~ zoF*xMXHszDh~pk203q;NI}gbNZx3D4g1Hw0+@>d3y5WW(#c;Ct?SkD&+qSFU43RQR zi=(pEs*KG)i2c~G7l0#3oV2x&BZ;*kQ=Aq?e@9(<{@*|RuhL6iMJR0xg={Lb`V34@3RmBaWO1=|NNby{fxdU6xPb}zK zXW_79TYb*`KR=U zMbR*CAP7zMnK2C%r>+2jg=g}%GX*kY-szGIHv;>nvx9aQGgqxE0{S0r8g!$RLCG!J ztQ>uM?GnOLM#=0&?|$xaIEDo7@?SYn!OJi*62*|Lpgkbo&^6Gy91816ZlAM8Y?PMz zi~3u{;D-TcQ!3*Pb~LyW#L+z`&kwHQ$I&aTbZ;wtIEe zKtQVRpeGO%8G{UE#Dmuwf*%yjTLrP*eSqdnX5nFG;!L&>{j%#U3p)O>F4I|w?S?Er zO0dU5oL`8Ho>FKKLy!Ks=K2MKVcSqBL`)W={|5X3xpuXd&CY&}9gSMM21n%YdiunU zd_V;MtMtDL`(la^-}(zNte?}s83uRVUtV-gI|cZ_c9b1vEr46lrRiiT820D@_{RrU zVlrF{Br)`MuE#rXqyp()j6P>z?KU_jX8`FO3$1q_nAFBc=CiQliHrp6pf7*Cc=JAB zX={alN7(lmawS8_KezLbF8){LDu!!N#_wJPMp2NM#lV394LT57)7Igi3g7^G0LI^c zcyL10%I&(y$a!tx%@Y^LamIVBY4!}d-%ST#aOiD7=c?Au^dmbHGxLvhra}DW?r%Ww z|7Kr)|CseYg?|D>1icD6kyWuQIh4o(F8_W?5EyLT&d9x8bo{Nv$f%bUTJG0F#gMoP zZA>~!?>InqKojf6_@5j5P4|7j`+vI|zkkKgmXoy`3~wRE&qOG>R>mh9KqgEWK>9Vl z?_+8nYClzcn8{l6%7t3D;?ywRq!VU9{exqEE)_i|ra_EnC)Voy=J zU-bQo71l^3Z$QvNtp!+dNJ4+l0cQ}w!I~FIWXK^h*)}hNT#)_=?SfwJ=fm&q1cv_s zT<4gBoMl>M<4C1)*>i>#ehT6R3${6`F-9dS&|>IWwnavn5gsj6pb_@OwR1+E#|fgJ z;QPAK4Dd;hW|F$VZ^iuHIiIqhfYNz0xjfnXvfxJGJFXxL zZX4X8XMsol=M*-gzbKY*=I2*3;P~ZD#pJ$!fT7bb*SgWf(x1QaA61p${<-b}1K!7e z&Rq_>tj}AGsG(T94;Fm?#g0B~yKHl<>#o(AFcfdqud4jrxA-TX%=_x^e*mcZAJpyt zBB;po{ELA4ci{TDOj?wAm}ihmYZwWce*Z&d(_4iQ$rve;O0oE3;cLl1WBEV6_#c&X zz#2&$b#)AZ0WJ629WAs0!O~2&)kDA$jdCdRh)y5dg(i2~IT{b(* ztUi(D=+xM~5&lWCK0f3wwOl24j?%d0*c08^?vjf>wa!6-AKvo{?fX39dtNxSb5xIV zkd{<&!71@sne$~8vr<1-i?G6CYA#rySuIDhW$V?Fx2vVXO1PamG z7(-*~)@*eTT=B)rw8T}eTX7zV!o}vvnbyw}oW;${t&4N?_IMmQj%`(0lB3;|UB-7& zl{NA#yURTz%?IWCqbg|Sm|LCF?pTVU(dTmGIn&cVNdnEUH(AxBE%nLxDY{!{C2D8I zI`V~6%2jE#_dI+trTErXo^~r5E|wsrkc9jApd>$U$Zx=;G4;aWl~M_u@og*Q@?Or(fj-Plmjzw#I@8t z8a-qKAI0_co}>vj7}+&8#>x419@9EdJIN7$B&8^}q@*~V6ek(gspNjOrY%!hJ1I4j zDS7mf@U4BT1+>`s5nqe8<ZNXd!X?IxOZhFM z%)Gl210I@%U$9la@?qYHd=c|lY2X%>>@It6=d=l5R-Lr+EeA?5>Qb+fRq6c(ZE3J=e(h1WlGK>E9W!AbWBZ~zwEmfcSJ>((f7II_ zqJ6A7RAzq+4BSmIRSUQ~FnVEm(K&vwd|U5$ek{7Des-!^ zb(8+xloC^Y3|=}cfM8BFEJo!t8P6V5R$d$xdF6Rs56tr1ckYKp!qgjO+{b>s{F79H z1MN?eDYB>BW>r3SuO>AmYAn5*_(g3n{92`c!YPt)qS{*vnJW?RwqJ;8qmvV`%+Qv$geC@xl(X-1Ab3!H4;RvvjcWQGweKgzS}5y<{b0QBgnZ)1}c< zCJ9F~s^db9)m2sq%|$64?w;7hrY2mB<$Ibdh3oJk%Hy{KAKvzmlS_19F{f`eiikDHZTFFu-A!=BXau%62?LpgD#~2^0_H(jZ z7Mcer1`A!3dcnT0=k4Ele4cySdTUvJ%CI|m+L9`pXhQOoPaF)W`d)SPaSd`q@c7pmi)&@zL%;;rn64ZrG{soiA>6lC!&?j{g>t)N)~j(lAqjpCccq(n?@< zK#fSMi@WSXocMr!L$D&Vy0}ubzh`5ky~SYCM)6!J_c;%DWIIJtI&mSgn_y%s`$l(o znARe4HsXtDYGoc(9Dlejyu&A$oY3NpHI4Tk{sz6!7*4?!r{JvmDEd~quCfbRvKfD5vZB%?fE%m9q$*-C*yF%rlIFPmZ?TgnrI&0qBlnqz+lNjk)fF({M}~? zY9ijH;)GuHWrxrTFGo=&-erx`5|-XZjkZ3x95)%2fA7?rH?uYgWW8uqoe7VJoT3{} zo2xMX3u=>z@WsMm+0CLm^af~W42N~1!p?ore&aE`GT@v7wiVpvcV0)HPj(_UJ1oqL zV646BKY3M0sc(wipC8jx&|ix)Y%nJo+dHMl1!QU;e(UWPp(?Y>J?Uylj7I+JnNp=$ zl*Y|q-j4%5YNMA2Dtd2>1tfPkALzw(q|S6^4L!)knqV)+pKGc`|5Hr!f7BuHPrbo^ zP@(@(o$Ej7JO8Oy`EQGM4UyG>g7;s@alZVg+W$XQh>j*aL*Z@uUcX;c=lgf8N5k#C zSJamI$@XbOxYOf1k};8cMHhyLSgPE5_T^^4|H2HpE}tsM;6EAc%|-^(iopLRumRh3 zFiQ#i|5fy_9{8&V{_27MP7hS8hD33}q-1e)i1|9xiEE+6H=T#~yi?35b((Nuvi$kl z`7%Y_mb~N_&`C8}EUY6?vA(k7YC4_*VYfI}8~yvL18I5GjT-N9)+A(3L)z@E3Kpx~Ts>d(ZE(7T``TuI3=jS7=Mh_xFDpB|8#Ei)$0xylB#^72j>@NTRL6_YQRUXDn{v{dzDKzsQqBS zOZ{)q>tsJXX~$U~{TS;PVZ+nCubRwKvJ=xpj;xffemr--*ypVZ-opfsi>#!Qb7e-| z&iZg<>XoC^>j++FjI|sU?E5Bl-(MdW{}OXfGrozBoo{Hm*(}AvRyRlMq`fh@zis;` zsm5(vGQ2JeW8w6nU{0g^%B!Nlkci%iLETCSbE==Q7H^)Gv|r1Ekp1^2oH#Fjv> zSS}%x>WG^uLRS3-8BNBRgoG1u!{vfMZQqd`mEuHk!%WPWKPd2Z)1lNFA1>|o!x_$j z?SYJa=d}8T?p8JQIT#;DnQ9nvo`HPGmE z*5Hqs!SIQqI-YL{vp)kCD6&1$`H0}!P_E%#ke`qe+~F1!6=-u{yfrMbypC4x8spY) z8G~3(zQ6F!aISiFWl1>y@YVND0lrdMU8w{37u`{Y1?7g$r&seoc*<_}S`??giE#_X z?Da!RjPpgD?B1_R8oV=;@yseZp*PGBdF&Wru%q(p#;2Y6F_@Kr_kM|}!W3&nPjno< z*7vJS;lP{0<3IAQWaG0ne5YCt7k6b$*Q093B@2sL5Zh6VHZ5_$A^+9nWy9vGt!=XV-pC3;;Y1E|Z?vgrFo!3n+#O3!k z^gr_7_wH%#4_|XD!gKPYvl^@xkd~2`pHr^iI_xprR5|CEo`1e;q~O(o5mJt29!rmV z?$$>^Q>!5dJiQw+9gj+1*2Ne&8P{RmF62ZzydL)qoE9}%4EL~N!ZQF*PvDmckWCV)AFSqas8m1$9+PZw33CE#x?b3T`R)=)b#30~rja$Gcg+{E^NF)!$X;DkluG19)q_Vk<3F0~_DxYdk|zsju(QxNHJu{yLgUGi z)P4H~s`I6L^$_xhFDFR)gv#m&Jv{UE?e=~7mi*(3-=Nr3!8_;Af4y7yFdM^%9B6dN zO6_x(8dgnETJ~C*>u*0*(qUcLjeV4DvS4=E)pXcZ5)o^MuUGp#ujQ_oocW?6+lSzc zDa*zS@rEZTCq2{Q!7s_w>23YAi9B|GIVVNKZniswCg>5+4EA-iTZk#~Xfeo;b7;Df zKslWuf4NUmra>xN_l5wwcB;}si{-T(_`OE-c32#hn&|!*oCE$Sg?jjYylWJ0fLjr)Xj57;si~@p!n)G|4r# z=aB|BE_;42Iaghrb_c^IbNoz;(!z6OQE!rUWJh7#zPZy~35f>1mL+)K9|VuYx1{tt zdd=lbz3`HFnRY%$8 zMhtAvS@)QC7d=YInQhNLwV+`Ut$eqEQZnm!dD5A2+^Q_s(Vav3RRh5ym@IJIyaD&j z%~|kFv+8g`OxdEZAH|uxRMT$VH9go}WPbI@ffs$`lG)ww-j&U~t>Yg}bV;wQcsVVp zXQPy!WtAS6w7Xy&NxAnhA@WTB`)97a<8Sc}Rydd0tb9qGa$9V)qolFK!-W;FeoVw8 zSyhw8YDyPJz#>T<*}x_#;}_YyRMES#UAck8mo|%+>XJG6-s3F3HGQyAG=0mfqB<$f z-oeZ6BvQxN@|n&Q&G^u*N)$a|Gs-g`CYqp-A8`*@&7+P$tvnQWQb z+pRo%FgV(%bm+i}r>0Wum0Sx0%M_=hl=xSMCf?B(boJO&Wwc*;-c@hjBCBIxF{5N~8lP0;dDDNfW6d0MLcCZAJwF5|Upd$zgSc*S1tNh4~!^xV00@!~&& z^M6xUeJRwVdia2GqxrCfyOCOdIKyb`xG~MyP>`BdET|T z=C9k3LwN-xqm7vA7!4bql3rDfJ?RNnHprZroVke!b>H*lIFt4q_qlySNon^i!WuJT z5#+7ql*A8HhlXkoc%EJ{%yx^>8mJ(U*ENJ4$(ntbHm2`@F zwIy8kO~uA0ysI2eZ;>Lc_H1@m$Gadp-c{P4Q>pis$T#Tb3s0BH#W$fw zHG4)AqK0K15>hjIk*}vaeInD_u^vK?KB#^uKHsu!Qs)iXBt}Z;W(W@WcB!<*Oj)ar zj!QfnW@X)^L%FjB8l>#o@s)aQRD*$RDf5KP23KrXKeB%FijvQi=2>iObf&(NtI}5| zrns}`=E45yjt$7n9t(`Jv$Zl}n4J*S?G}&H^0;_B;K%zNgz{_wY+=^JffY56nwoxE zlmLDP8x>Yqg7r2J>N=w0uyx;?Y1$Xg(bgqYFJ?O*M~2PU1r2WsN`E5uOb&JAY~21I zEi=!HQgT0)&FVJdq8+oMcS$7eEOI{m=GDOh=89+5=0$$l^+zMUBjb-qea@f92k3Q~ zQEg4IRP6s--W=1+Q@$tDwK>!O=pVSgAY+x3+@*Qv)2^C4l}WBMfAX|-&f>){5A9uB zHB~Wgt;Z_U5LMYs-{o^t&+5%9;`dv7Rm(&{i- q_^{H9lc)A9ZrQwf()n(ODhDPFj0^}7fb959OTZyL#Dy9E-vj{Bm4(y* literal 0 HcmV?d00001 diff --git a/docs/migration/evidence/slice-08/heaven-heart-dark-1080.png b/docs/migration/evidence/slice-08/heaven-heart-dark-1080.png new file mode 100644 index 0000000000000000000000000000000000000000..5057971e38739b369f1e6275fa8d00378241ff39 GIT binary patch literal 73004 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 M@W1=Py`SIzKdJP}-v9sr literal 0 HcmV?d00001 diff --git a/docs/migration/evidence/slice-08/heaven-trend-1080.png b/docs/migration/evidence/slice-08/heaven-trend-1080.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba600e61bb7b71bf6465408dce663a0897b13ba GIT binary patch literal 77780 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 EFU5i;H~;_u literal 0 HcmV?d00001 diff --git a/docs/migration/保真迁移状态.json b/docs/migration/保真迁移状态.json index 6e02594..4ca70fb 100644 --- a/docs/migration/保真迁移状态.json +++ b/docs/migration/保真迁移状态.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "updated_at": "2026-07-31T04:17:23+08:00", + "updated_at": "2026-07-31T08:55:17+08:00", "status": "active", "migration_mode": "behavior_preserving_source_migration", "source_of_truth": "current_original_webapp_runtime_and_source", @@ -9,10 +9,10 @@ "failed_roots": [ "next" ], - "current_slice": "slice-08-heaven-trend-fortune-heart", - "last_completed_slice": "slice-07-mentor-skills-llm-streaming", - "last_checkpoint": "xiaobai-preservation-slice-07-20260731", - "next_action": "capture_slice-08_heaven_trend_fortune_heart_and_animation_contracts_then_move_original_implementations", + "current_slice": "slice-09-review-watchlist-notes-journal-alerts-assistant", + "last_completed_slice": "slice-08-heaven-trend-fortune-heart", + "last_checkpoint": "xiaobai-preservation-slice-08-20260731", + "next_action": "capture_slice-09_review_private_data_and_assistant_contracts_then_move_original_implementations", "authoritative_documents": [ "AGENTS.md", "docs/migration/原版保真迁移总纲.md", diff --git a/docs/migration/保真迁移账本.md b/docs/migration/保真迁移账本.md index a1fea69..cf44cf7 100644 --- a/docs/migration/保真迁移账本.md +++ b/docs/migration/保真迁移账本.md @@ -1,6 +1,6 @@ # 小白复盘保真迁移账本 -> 当前状态:正式迁移,切片07“问师、模型 Skill 与 LLM 流式链路”已完成 +> 当前状态:正式迁移,切片08“问天、观势、观气与观心”已完成 本账本是上下文恢复和人工审计的连续记录。任何迁移提交必须在同一提交中更新本文件及 `保真迁移状态.json`。 @@ -27,6 +27,7 @@ | 2026-07-31 | `xiaobai-preservation-slice-05-20260731` | 集合竞价、题材库、人气热榜与龙虎榜原实现归位 | 自动、API、数据库与浏览器差分通过,进入切片06 | | 2026-07-31 | `xiaobai-preservation-slice-06-20260731` | 智能选股、自定义选股与策略持续跟踪原实现归位 | 自动、API、数据库与浏览器差分通过,进入切片07 | | 2026-07-31 | `xiaobai-preservation-slice-07-20260731` | 问师、模型Skill与LLM流式链路原实现归位 | 自动、API、数据库、Skill与浏览器差分通过,进入切片08 | +| 2026-07-31 | `xiaobai-preservation-slice-08-20260731` | 问天、观势、观气与观心原实现归位 | 自动、API、数据库、动画与浏览器差分通过,进入切片09 | ## 资产处置登记 @@ -58,6 +59,8 @@ | 问师消息与偏好方法 | 持久化 | 用户对话、置顶和排序 | 按职责机械移动并保持Mixin原接口 | `app/backend/features/mentor/repository.py` | 5个方法AST一致;相关表逐行一致 | 已移动 | | `llm_stream.py`与模型访问方法 | 公共模型能力 | 问师、问天、复盘助手与策略编译 | 移入唯一模型边界并保留兼容别名 | `app/backend/llm/` | 流式文件哈希一致;21个服务方法与既有用户边界一致 | 已移动 | | 公开`游资skills` | 运行资产 | 问师模型库 | 原样保留 | `app/游资skills/` | 190个文件逐路径和SHA-256一致 | 已复制 | +| `heaven_agent.py`、`heaven_engine.py`与问天服务 | 业务计算 | 观势、观气、观心 | 机械移动并保留兼容别名 | `app/backend/features/heaven/` | Agent文件哈希、引擎定义AST、20个服务方法、6个API及真实动画页面等价 | 已移动 | +| 问天历史方法 | 持久化 | 三类解读历史与当日解运复用 | 按职责机械移动 | `app/backend/features/heaven/repository.py` | 5个方法AST一致;62个schema对象及6张关键表逐行一致 | 已移动 | 处置只允许:`原样保留`、`移动`、`合并重复`、`待定`、`确认废弃`。 @@ -145,6 +148,16 @@ - 回档:标签`xiaobai-preservation-slice-07-20260731`。 - 完整证据:`docs/migration/evidence/slice-07/README.md`。 +已完成切片:`slice-08-heaven-trend-fortune-heart`。 + +- 原版基线:提交`2919229`,即切片07回档点。 +- 迁移范围:问天Agent、历法/卦象引擎、20个服务方法、5个持久化方法及3个HTTP入口。 +- 兼容边界:根级`heaven_agent.py`和`heaven_engine.py`指向正式模块对象;原动画、DOM、JS与CSS未改动。 +- API与数据库:6个固定输入真实API完全一致;62个schema对象及6张关键表逐行一致。 +- 验收:原版231项、迁移版280项Python测试、7项切片源码等价测试、45项Playwright及1080P三模式动画页面通过。 +- 回档:标签`xiaobai-preservation-slice-08-20260731`。 +- 完整证据:`docs/migration/evidence/slice-08/README.md`。 + ## 决策记录 | 日期 | 决策 | 原因 |