From 35ae079de787c8db53cfa575b3d83583da25e0d4 Mon Sep 17 00:00:00 2001 From: leefer Date: Thu, 30 Jul 2026 07:08:13 +0800 Subject: [PATCH] rebuild(stage-11): deliver deterministic heaven workflows --- next/backend/bootstrap/container.py | 14 +- next/backend/data/gateway.py | 61 +- next/backend/data/heaven.py | 485 ++++ next/backend/data/providers/base.py | 8 + next/backend/data/providers/eastmoney.py | 10 + next/backend/data/providers/ifind.py | 10 + next/backend/data/providers/tushare.py | 174 +- .../{features/market => data}/sentiment.py | 0 .../database/migrations/m0009_heaven.py | 53 + next/backend/database/migrations/registry.py | 2 + next/backend/features/heaven/__init__.py | 3 + next/backend/features/heaven/fortune.py | 259 ++ next/backend/features/heaven/hexagram.py | 77 + next/backend/features/heaven/prompt.py | 37 + next/backend/features/heaven/repository.py | 139 + next/backend/features/heaven/routes.py | 110 + next/backend/features/heaven/schemas.py | 34 + next/backend/features/heaven/service.py | 268 ++ next/backend/features/heaven/trend.py | 353 +++ next/backend/features/market/sync.py | 2 +- next/backend/http/router.py | 2 + next/config/heaven/iching_zh.json | 2393 +++++++++++++++++ next/docs/evidence/stage-11/acceptance.md | 49 + .../stage-11/heaven-dark-1920x1080.jpg | Bin 0 -> 99400 bytes .../heaven-interpret-loading-dark.jpg | Bin 0 -> 80972 bytes .../stage-11/heaven-light-390x844.jpg | Bin 0 -> 28222 bytes next/frontend/index.html | 1 + next/frontend/public/heaven-loading.js | 723 +++++ next/frontend/src/app/views/WorkspaceView.vue | 2 + next/frontend/src/env.d.ts | 8 + next/frontend/src/main.ts | 3 + .../src/pages/heaven/FortunePanel.vue | 67 + next/frontend/src/pages/heaven/HeartPanel.vue | 137 + next/frontend/src/pages/heaven/HeavenPage.vue | 84 + .../src/pages/heaven/HexagramGraphic.vue | 27 + .../src/pages/heaven/InterpretDialog.vue | 104 + next/frontend/src/pages/heaven/TrendPanel.vue | 110 + .../src/pages/heaven/heartTiming.test.ts | 19 + next/frontend/src/pages/heaven/heartTiming.ts | 16 + next/frontend/src/shared/api/heaven.ts | 63 + .../src/shared/styles/heaven-fortune.css | 127 + .../src/shared/styles/heaven-heart.css | 182 ++ next/frontend/src/shared/styles/heaven.css | 408 +++ next/frontend/src/shared/styles/tokens.css | 19 + next/requirements.txt | 1 + next/tests/e2e/stage11.spec.js | 174 ++ next/tests/test_heaven.py | 416 +++ next/tests/test_market_data.py | 8 +- next/tests/test_migrations.py | 5 +- 49 files changed, 7208 insertions(+), 39 deletions(-) create mode 100644 next/backend/data/heaven.py rename next/backend/{features/market => data}/sentiment.py (100%) create mode 100644 next/backend/database/migrations/m0009_heaven.py create mode 100644 next/backend/features/heaven/__init__.py create mode 100644 next/backend/features/heaven/fortune.py create mode 100644 next/backend/features/heaven/hexagram.py create mode 100644 next/backend/features/heaven/prompt.py create mode 100644 next/backend/features/heaven/repository.py create mode 100644 next/backend/features/heaven/routes.py create mode 100644 next/backend/features/heaven/schemas.py create mode 100644 next/backend/features/heaven/service.py create mode 100644 next/backend/features/heaven/trend.py create mode 100644 next/config/heaven/iching_zh.json create mode 100644 next/docs/evidence/stage-11/acceptance.md create mode 100644 next/docs/evidence/stage-11/heaven-dark-1920x1080.jpg create mode 100644 next/docs/evidence/stage-11/heaven-interpret-loading-dark.jpg create mode 100644 next/docs/evidence/stage-11/heaven-light-390x844.jpg create mode 100644 next/frontend/public/heaven-loading.js create mode 100644 next/frontend/src/pages/heaven/FortunePanel.vue create mode 100644 next/frontend/src/pages/heaven/HeartPanel.vue create mode 100644 next/frontend/src/pages/heaven/HeavenPage.vue create mode 100644 next/frontend/src/pages/heaven/HexagramGraphic.vue create mode 100644 next/frontend/src/pages/heaven/InterpretDialog.vue create mode 100644 next/frontend/src/pages/heaven/TrendPanel.vue create mode 100644 next/frontend/src/pages/heaven/heartTiming.test.ts create mode 100644 next/frontend/src/pages/heaven/heartTiming.ts create mode 100644 next/frontend/src/shared/api/heaven.ts create mode 100644 next/frontend/src/shared/styles/heaven-fortune.css create mode 100644 next/frontend/src/shared/styles/heaven-heart.css create mode 100644 next/frontend/src/shared/styles/heaven.css create mode 100644 next/tests/e2e/stage11.spec.js create mode 100644 next/tests/test_heaven.py diff --git a/next/backend/bootstrap/container.py b/next/backend/bootstrap/container.py index f1aa8af..5e2d1bc 100644 --- a/next/backend/bootstrap/container.py +++ b/next/backend/bootstrap/container.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from backend.bootstrap.settings import Settings +from backend.bootstrap.settings import PROJECT_ROOT, Settings from backend.data.gateway import DataGateway from backend.data.policy import DataSourcePolicy from backend.data.providers import EastmoneyProvider, IfindProvider, TushareProvider @@ -16,6 +16,8 @@ from backend.features.accounts.service import ( AccountService, MembershipService, ) +from backend.features.heaven.repository import HeavenRepository +from backend.features.heaven.service import HeavenService from backend.features.market import MarketService from backend.features.market.insights import MarketInsightService from backend.features.market.sync import MarketSnapshotService @@ -43,6 +45,7 @@ class ApplicationContainer: screener: ScreenerService llm: LLMGateway mentor: MentorService + heaven: HeavenService def build_container(settings: Settings) -> ApplicationContainer: @@ -89,6 +92,14 @@ def build_container(settings: Settings) -> ApplicationContainer: gateway, llm, ) + heaven = HeavenService( + database, + HeavenRepository(), + gateway, + accounts, + llm, + PROJECT_ROOT / "config" / "heaven" / "iching_zh.json", + ) return ApplicationContainer( settings=settings, database=database, @@ -101,4 +112,5 @@ def build_container(settings: Settings) -> ApplicationContainer: screener=screener, llm=llm, mentor=mentor, + heaven=heaven, ) diff --git a/next/backend/data/gateway.py b/next/backend/data/gateway.py index faa310b..a1226df 100644 --- a/next/backend/data/gateway.py +++ b/next/backend/data/gateway.py @@ -16,6 +16,7 @@ from backend.data.contracts import ( SnapshotState, TradeContext, ) +from backend.data.heaven import historical_payload, realtime_payload, should_use_realtime from backend.data.policy import DataSourcePolicy from backend.data.providers.base import MarketDataProvider, ProviderError from backend.data.quality import DataQualityError, require_quality @@ -217,6 +218,34 @@ class DataGateway: ) return payload + def heaven_trend_inputs( + self, query: str, requested_date: str, now: datetime | None = None + ) -> dict[str, Any]: + clock = now or datetime.now(SHANGHAI) + context = self.trade_context(requested_date, clock) + if context.actual_date is None: + raise MarketDataUnavailable("等待管理员首次同步真实收盘行情") + stock = self._resolve_stock_query(query) + provider = self._provider(DataSource.TUSHARE) + self._policy.assert_allowed(provider.source, DataUsage.CALCULATION) + if should_use_realtime(requested_date, context, clock): + dates = self.trading_dates(requested_date, 2) + if len(dates) < 2 or dates[0] != requested_date: + raise MarketDataUnavailable("目标日期不是有效交易日") + raw = provider.heaven_realtime_inputs(stock.identifier, dates[0], dates[1]) + return realtime_payload( + self._database, + self._repository, + stock, + dates[0], + dates[1], + raw, + clock, + ) + return historical_payload( + self._database, self._repository, stock, context.actual_date, provider + ) + def search(self, query: str) -> tuple[MarketEntity, ...]: with self._database.read() as connection: return self._repository.search(connection, query) @@ -310,6 +339,29 @@ class DataGateway: return MarketEntity("stock", f"{normalized}.{suffix}", normalized, normalized) raise MarketDataUnavailable("未找到该行情标的") + def _resolve_stock_query(self, query: str) -> MarketEntity: + normalized = query.strip() + if not normalized: + raise MarketDataUnavailable("请输入股票代码或股票名称") + with self._database.read() as connection: + matches = tuple( + item + for item in self._repository.search(connection, normalized, 16) + if item.entity_type == "stock" + ) + exact = [ + item + for item in matches + if item.code.casefold() == normalized.casefold() + or item.identifier.casefold() == normalized.casefold() + or item.name.casefold() == normalized.casefold() + ] + if len(exact) == 1: + return exact[0] + if len(exact) > 1: + raise MarketDataUnavailable("股票名称存在重名,请输入六位代码") + raise MarketDataUnavailable("未找到该股票,请检查代码或名称") + def _save_chart(self, series: ChartSeries) -> None: payload = { "previous_close": series.previous_close, @@ -485,5 +537,10 @@ def _number(value: Any) -> float: def _optional_number(value: Any) -> float | None: - number = _number(value) - return number if number > 0 else None + if value is None or value == "": + return None + try: + number = float(value) + except (TypeError, ValueError): + return None + return number if number == number else None diff --git a/next/backend/data/heaven.py b/next/backend/data/heaven.py new file mode 100644 index 0000000..991e415 --- /dev/null +++ b/next/backend/data/heaven.py @@ -0,0 +1,485 @@ +from __future__ import annotations + +import json +from datetime import datetime, time +from typing import Any + +from backend.data.contracts import MarketEntity, ProviderResult, SnapshotState, TradeContext +from backend.data.providers.base import MarketDataProvider +from backend.data.repository import MarketRepository +from backend.data.sentiment import calculate_sentiment +from backend.database.connection import Database + + +def should_use_realtime(requested_date: str, context: TradeContext, clock: datetime) -> bool: + today = clock.date().isoformat() + if requested_date != today or clock.time() < time(9, 15): + return False + return not ( + context.actual_date == today + and context.state in {SnapshotState.FINAL, SnapshotState.ARCHIVE} + ) + + +def historical_payload( + database: Database, + repository: MarketRepository, + stock: MarketEntity, + trade_date: str, + provider: MarketDataProvider, +) -> dict[str, Any]: + raw = provider.heaven_inputs(stock.identifier, trade_date) + daily_rows = _rows(raw, "daily") + basic_rows = _rows(raw, "daily_basic") + member_rows = _rows(raw, "members") + sector_rows = _rows(raw, "sector_daily") + index_rows = _rows(raw, "indices") + quote = next((row for row in daily_rows if row.get("ts_code") == stock.identifier), {}) + basic = next((row for row in basic_rows if row.get("ts_code") == stock.identifier), {}) + with database.read() as connection: + summary_row = repository.latest_summary(connection, trade_date) + history_rows = repository.summaries(connection, trade_date, 6) + summary = json.loads(str(summary_row["payload_json"])) if summary_row else {} + overview = summary.get("overview") or {} + sentiment = summary.get("sentiment") or {} + event = _stock_event(summary, stock.identifier) + amount = _number(quote.get("amount")) + member_changes = [_number(row.get("change")) for row in member_rows if bool(row.get("quoted"))] + leader = max( + (row for row in member_rows if bool(row.get("quoted"))), + key=lambda row: _number(row.get("change")), + default={}, + ) + sector_quote = sector_rows[0] if sector_rows else {} + sector_name = str((member_rows[0] if member_rows else {}).get("sector_name") or "") + return { + "trade_date": trade_date, + "mode": "historical", + "stock": { + "identifier": stock.identifier, + "code": stock.code, + "name": stock.name, + "trade_date": _display_date(quote.get("trade_date")), + "quote_kind": "daily", + "change": _optional_number(quote.get("pct_chg")), + "amount_billion": round(amount / 100_000, 4), + "amount_percentile": _percentile_rank( + amount, sorted(_number(row.get("amount")) for row in daily_rows) + ), + "turnover_rate": _optional_number(basic.get("turnover_rate")), + "seal_amount_million": _number(event.get("seal_amount")) / 1_000_000, + "open_times": event.get("open_times", 0), + "streak": event.get("streak", 0), + "status": event.get("status", "普通"), + }, + "sector": { + "name": sector_name, + "code": str((member_rows[0] if member_rows else {}).get("sector_code") or ""), + "taxonomy": "申万二级" if sector_name else "", + "trade_date": _display_date(sector_quote.get("trade_date")) or trade_date, + "quote_kind": "daily", + "change": _optional_number(sector_quote.get("pct_change")), + "up_count": sum(value > 0 for value in member_changes), + "down_count": sum(value < 0 for value in member_changes), + "member_count": len(member_rows), + "quoted_count": len(member_changes), + "coverage": len(member_changes) / max(len(member_rows), 1), + "member_equal_change": ( + sum(member_changes) / len(member_changes) if member_changes else None + ), + "leader": str(leader.get("name") or ""), + "leading_pct": _optional_number(leader.get("change")), + }, + "market": _market( + trade_date, + "daily", + overview, + sentiment.get("score"), + _history_amounts(history_rows[:-1]), + ), + "indices": [ + { + "identifier": str(row.get("ts_code") or ""), + "trade_date": _display_date(row.get("trade_date")), + "quote_kind": "daily", + "change": _optional_number(row.get("pct_chg")), + } + for row in index_rows + ], + } + + +def realtime_payload( + database: Database, + repository: MarketRepository, + stock: MarketEntity, + trade_date: str, + previous_trade_date: str, + raw: dict[str, ProviderResult | None], + clock: datetime, +) -> dict[str, Any]: + realtime_rows = [ + row + for row in _rows(raw, "realtime") + if _quote_date(row) == trade_date + and _valid_quote(row) + and (clock.time() < time(15) or _quote_time(row) >= "15:00:00") + ] + quote_map = {str(row.get("ts_code") or ""): row for row in realtime_rows} + quote = quote_map.get(stock.identifier, {}) + members = _rows(raw, "members") + capital = {str(row.get("ts_code") or ""): row for row in _rows(raw, "capital")} + limits = {str(row.get("ts_code") or ""): row for row in _rows(raw, "price_limits")} + suspended = {str(row.get("ts_code") or "") for row in _rows(raw, "suspensions")} + market_rows = [row for row in realtime_rows if str(row.get("ts_code") or "") in capital] + market_turnover = _average(_turnovers(market_rows, capital)) + stock_turnover = _turnover(quote, capital.get(stock.identifier, {})) + history_volumes = sorted( + ( + (_display_date(row.get("trade_date")), _number(row.get("vol"))) + for row in _rows(raw, "stock_history") + if _number(row.get("vol")) > 0 + ), + key=lambda item: item[0], + )[-5:] + average_volume = _average([value for _, value in history_volumes]) + activity = ( + _number(quote.get("vol")) / 100 / (average_volume * _session_progress(clock.time())) + if average_volume + else 0 + ) + with database.read() as connection: + history_rows = repository.summaries(connection, previous_trade_date, 250) + history = [json.loads(str(row["payload_json"])) for row in history_rows] + prior = history[-1] if history else {} + current_limits = _current_limits(market_rows, limits, prior) + overview = _realtime_overview(market_rows, current_limits) + yesterday = _yesterday(prior.get("limits") or [], quote_map, current_limits) + sentiment = calculate_sentiment( + {"overview": overview, "limits": current_limits["up"], "yesterday_limits": yesterday}, + history, + ) + member_codes = [str(row.get("ts_code") or "") for row in members] + member_quotes = [quote_map[code] for code in member_codes if code in quote_map] + explained = len(member_quotes) + sum(code in suspended for code in member_codes) + changes = [_quote_change(row) for row in member_quotes] + leader = max(member_quotes, key=_quote_change, default={}) + sector_quote = next( + ( + row + for row in _rows(raw, "sector_realtime") + if _quote_date(row) == trade_date + and (clock.time() < time(15) or _quote_time(row) >= "15:00:00") + ), + {}, + ) + sector_turnover = _average(_turnovers(member_quotes, capital)) + sector_name = str((members[0] if members else {}).get("sector_name") or "") + amount = _number(quote.get("amount")) + status, streak = _status(stock.identifier, quote, limits, prior) + return { + "trade_date": trade_date, + "mode": "intraday", + "stock": { + "identifier": stock.identifier, + "code": stock.code, + "name": stock.name, + "trade_date": _quote_date(quote), + "quote_kind": "realtime", + "change": _optional_number(_quote_change(quote)) if quote else None, + "amount_billion": round(amount / 100_000_000, 4), + "amount_percentile": _percentile_rank( + amount, sorted(_number(row.get("amount")) for row in market_rows) + ), + "turnover_rate": stock_turnover or None, + "turnover_relative": stock_turnover / market_turnover if market_turnover else None, + "volume_activity_ratio": activity or None, + "seal_amount_million": 0, + "open_times": 0, + "streak": streak, + "status": status, + }, + "sector": { + "name": sector_name, + "code": str((members[0] if members else {}).get("sector_code") or ""), + "taxonomy": "申万二级" if sector_name else "", + "trade_date": _quote_date(sector_quote), + "quote_kind": "realtime", + "change": _optional_number(_quote_change(sector_quote)) if sector_quote else None, + "up_count": sum(value > 0 for value in changes), + "down_count": sum(value < 0 for value in changes), + "member_count": len(member_codes), + "quoted_count": explained, + "coverage": explained / max(len(member_codes), 1), + "member_equal_change": _average(changes) if changes else None, + "relative_turnover": (sector_turnover / market_turnover if market_turnover else None), + "leader": str(leader.get("name") or ""), + "leading_pct": _optional_number(_quote_change(leader)) if leader else None, + }, + "market": _market( + trade_date, + "realtime", + overview, + sentiment.get("score"), + _history_amounts(history_rows), + ), + "indices": [ + { + "identifier": identifier, + "trade_date": _quote_date(quote_map.get(identifier, {})), + "quote_kind": "realtime", + "change": ( + _optional_number(_quote_change(quote_map[identifier])) + if identifier in quote_map + else None + ), + } + for identifier in ("000001.SH", "399001.SZ", "399006.SZ") + ], + } + + +def _rows(values: dict[str, ProviderResult | None], key: str) -> tuple[dict[str, Any], ...]: + result = values.get(key) + return result.rows if isinstance(result, ProviderResult) else () + + +def _history_amounts(rows: tuple[Any, ...]) -> list[float]: + return [ + _number((json.loads(str(row["payload_json"])).get("overview") or {}).get("amount")) + for row in rows + if row["payload_json"] + ] + + +def _market( + trade_date: str, + quote_kind: str, + overview: dict[str, Any], + sentiment_score: Any, + history_amounts: list[float], +) -> dict[str, Any]: + amount = _number(overview.get("amount")) + average = _average(history_amounts[-5:]) if history_amounts else amount + return { + "trade_date": trade_date, + "quote_kind": quote_kind, + "sentiment_score": _optional_number(sentiment_score), + "seal_rate": _optional_number(overview.get("seal_rate")), + "amount_billion": amount / 100_000_000, + "average_amount_billion": average / 100_000_000, + "up_count": overview.get("up_count"), + "down_count": overview.get("down_count"), + "limit_up_count": overview.get("limit_up"), + "limit_down_count": overview.get("limit_down"), + } + + +def _stock_event(summary: dict[str, Any], identifier: str) -> dict[str, Any]: + for key in ("limits", "broken", "down_limits"): + for row in summary.get(key) or []: + if str(row.get("identifier") or "") == identifier: + return dict(row) + return {"status": "普通", "streak": 0, "open_times": 0, "seal_amount": 0} + + +def _quote_date(row: dict[str, Any]) -> str: + return _display_date(row.get("trade_time") or row.get("trade_date")) + + +def _quote_time(row: dict[str, Any]) -> str: + value = str(row.get("trade_time") or "") + if " " in value: + return value.split(" ", 1)[1][:8] + compact = "".join(character for character in value if character.isdigit()) + return f"{compact[8:10]}:{compact[10:12]}:{compact[12:14]}" if len(compact) >= 14 else "" + + +def _valid_quote(row: dict[str, Any]) -> bool: + return _number(row.get("close")) > 0 and _number(row.get("pre_close")) > 0 + + +def _quote_change(row: dict[str, Any]) -> float: + for key in ("pct_chg", "pct_change"): + value = _optional_number(row.get(key)) + if value is not None: + return value + close = _number(row.get("close")) + previous = _number(row.get("pre_close")) + return (close / previous - 1) * 100 if close and previous else 0 + + +def _turnover(row: dict[str, Any], capital: dict[str, Any]) -> float: + float_share = _number(capital.get("float_share")) + return _number(row.get("vol")) / float_share / 100 if float_share else 0 + + +def _turnovers(rows: list[dict[str, Any]], capital: dict[str, dict[str, Any]]) -> list[float]: + values = [_turnover(row, capital.get(str(row.get("ts_code") or ""), {})) for row in rows] + return [value for value in values if value > 0] + + +def _session_progress(current: time) -> float: + if current <= time(9, 30): + return 0.05 + if current <= time(11, 30): + return max(0.05, min(0.5, (current.hour * 60 + current.minute - 570) / 240)) + if current < time(13): + return 0.5 + if current <= time(15): + return max(0.5, min(1.0, 0.5 + (current.hour * 60 + current.minute - 780) / 240)) + return 1.0 + + +def _at_price(value: Any, target: Any) -> bool: + price = _number(value) + limit = _number(target) + return bool(limit and abs(price - limit) <= max(0.005, limit * 0.0002)) + + +def _prior_limits(summary: dict[str, Any]) -> dict[str, dict[str, Any]]: + return { + str(row.get("identifier") or ""): row + for row in summary.get("limits") or [] + if row.get("identifier") + } + + +def _current_limits( + market_rows: list[dict[str, Any]], + price_limits: dict[str, dict[str, Any]], + prior: dict[str, Any], +) -> dict[str, list[dict[str, Any]]]: + result: dict[str, list[dict[str, Any]]] = {"up": [], "down": [], "broken": []} + prior_map = _prior_limits(prior) + for quote in market_rows: + identifier = str(quote.get("ts_code") or "") + bounds = price_limits.get(identifier, {}) + is_up = _at_price(quote.get("close"), bounds.get("up_limit")) + is_down = _at_price(quote.get("close"), bounds.get("down_limit")) + touched = ( + _number(bounds.get("up_limit")) > 0 + and _number(quote.get("high")) >= _number(bounds.get("up_limit")) * 0.9998 + ) + previous_streak = int(_number(prior_map.get(identifier, {}).get("streak"))) + row = { + "identifier": identifier, + "code": identifier.split(".")[0], + "name": str(quote.get("name") or "").strip(), + "amount": _number(quote.get("amount")), + "change": _quote_change(quote), + "streak": previous_streak + 1 if is_up and previous_streak else 1 if is_up else 0, + } + if is_up: + result["up"].append(row) + elif is_down: + result["down"].append(row) + elif touched: + result["broken"].append(row) + return result + + +def _realtime_overview( + market_rows: list[dict[str, Any]], limit_rows: dict[str, list[dict[str, Any]]] +) -> dict[str, Any]: + changes = [_quote_change(row) for row in market_rows] + up_count = sum(value > 0 for value in changes) + down_count = sum(value < 0 for value in changes) + limits = len(limit_rows["up"]) + broken = len(limit_rows["broken"]) + return { + "up_count": up_count, + "down_count": down_count, + "flat_count": len(changes) - up_count - down_count, + "limit_up": limits, + "limit_down": len(limit_rows["down"]), + "broken": broken, + "seal_rate": round(limits / max(limits + broken, 1) * 100, 1), + "amount": sum(_number(row.get("amount")) for row in market_rows), + } + + +def _yesterday( + previous_limits: list[dict[str, Any]], + quotes: dict[str, dict[str, Any]], + current: dict[str, list[dict[str, Any]]], +) -> list[dict[str, Any]]: + up_codes = {str(row["identifier"]) for row in current["up"]} + broken_codes = {str(row["identifier"]) for row in current["broken"]} + down_codes = {str(row["identifier"]) for row in current["down"]} + rows = [] + for previous in previous_limits: + identifier = str(previous.get("identifier") or "") + quote = quotes.get(identifier) + if not quote: + continue + change = _quote_change(quote) + outcome = ( + "晋级" + if identifier in up_codes + else "炸板" + if identifier in broken_codes + else "跌停" + if identifier in down_codes + else "红盘" + if change > 0 + else "断板" + ) + rows.append({"identifier": identifier, "current_change": change, "outcome": outcome}) + return rows + + +def _status( + identifier: str, + quote: dict[str, Any], + price_limits: dict[str, dict[str, Any]], + prior: dict[str, Any], +) -> tuple[str, int]: + if not quote: + return "普通", 0 + bounds = price_limits.get(identifier, {}) + if _at_price(quote.get("close"), bounds.get("up_limit")): + return "涨停", int(_number(_prior_limits(prior).get(identifier, {}).get("streak"))) + 1 + if _at_price(quote.get("close"), bounds.get("down_limit")): + return "跌停", 0 + if ( + _number(bounds.get("up_limit")) + and _number(quote.get("high")) >= _number(bounds.get("up_limit")) * 0.9998 + ): + return "炸板", 0 + return "普通", 0 + + +def _average(values: list[float]) -> float: + return sum(values) / len(values) if values else 0 + + +def _number(value: Any) -> float: + try: + return float(value or 0) + except (TypeError, ValueError): + return 0.0 + + +def _optional_number(value: Any) -> float | None: + if value is None or value == "": + return None + try: + number = float(value) + except (TypeError, ValueError): + return None + return number if number == number else None + + +def _display_date(value: Any) -> str: + compact = str(value or "").replace("-", "")[:8] + if len(compact) != 8 or not compact.isdigit(): + return "" + return f"{compact[:4]}-{compact[4:6]}-{compact[6:]}" + + +def _percentile_rank(value: float, ordered: list[float]) -> float | None: + valid = [item for item in ordered if item > 0] + if value <= 0 or not valid: + return None + return round(sum(item <= value for item in valid) / len(valid) * 100, 2) diff --git a/next/backend/data/providers/base.py b/next/backend/data/providers/base.py index d7c9c98..ca90b30 100644 --- a/next/backend/data/providers/base.py +++ b/next/backend/data/providers/base.py @@ -29,6 +29,14 @@ class MarketDataProvider(Protocol): def sector_members(self, representative: str, trade_date: str) -> ProviderResult: ... + def heaven_inputs( + self, representative: str, trade_date: str + ) -> dict[str, ProviderResult | None]: ... + + def heaven_realtime_inputs( + self, representative: str, trade_date: str, previous_trade_date: str + ) -> dict[str, ProviderResult | None]: ... + def market_insight( self, kind: str, diff --git a/next/backend/data/providers/eastmoney.py b/next/backend/data/providers/eastmoney.py index 0533638..c259c22 100644 --- a/next/backend/data/providers/eastmoney.py +++ b/next/backend/data/providers/eastmoney.py @@ -102,6 +102,16 @@ class EastmoneyProvider: def sector_members(self, representative: str, trade_date: str) -> ProviderResult: raise ProviderError("The display provider is not the constituent authority") + def heaven_inputs( + self, representative: str, trade_date: str + ) -> dict[str, ProviderResult | None]: + raise ProviderError("The display provider cannot supply deterministic Heaven inputs") + + def heaven_realtime_inputs( + self, representative: str, trade_date: str, previous_trade_date: str + ) -> dict[str, ProviderResult | None]: + raise ProviderError("The display provider cannot supply deterministic Heaven inputs") + def market_insight( self, kind: str, diff --git a/next/backend/data/providers/ifind.py b/next/backend/data/providers/ifind.py index f72fef6..0525e42 100644 --- a/next/backend/data/providers/ifind.py +++ b/next/backend/data/providers/ifind.py @@ -92,6 +92,16 @@ class IfindProvider: def sector_members(self, representative: str, trade_date: str) -> ProviderResult: raise ProviderError("iFinD is not the Shenwan constituent authority") + def heaven_inputs( + self, representative: str, trade_date: str + ) -> dict[str, ProviderResult | None]: + raise ProviderError("iFinD deterministic Heaven inputs are not enabled") + + def heaven_realtime_inputs( + self, representative: str, trade_date: str, previous_trade_date: str + ) -> dict[str, ProviderResult | None]: + raise ProviderError("iFinD deterministic Heaven inputs are not enabled") + def market_insight( self, kind: str, diff --git a/next/backend/data/providers/tushare.py b/next/backend/data/providers/tushare.py index 40c915d..9d77dc2 100644 --- a/next/backend/data/providers/tushare.py +++ b/next/backend/data/providers/tushare.py @@ -132,36 +132,9 @@ class TushareProvider: def sector_members(self, representative: str, trade_date: str) -> ProviderResult: target = _compact(trade_date) - memberships = self._membership_rows({"ts_code": representative}) - active = [row for row in memberships if _active_on(row, target)] - if not active: - raise ProviderError("未找到该股票在目标日期的申万行业") - industry = max( - active, - key=lambda row: ( - str(row.get("in_date") or ""), - str(row.get("l2_code") or ""), - ), - ) + industry, members = self._sector_memberships(representative, target) sector_code = str(industry.get("l2_code") or "") sector_name = str(industry.get("l2_name") or "").strip() - if not sector_code: - raise ProviderError("该股票缺少申万二级行业") - members = [ - row - for row in self._membership_rows({"l2_code": sector_code}) - if _active_on(row, target) - ] - deduplicated: dict[str, dict[str, Any]] = {} - for row in members: - code = str(row.get("ts_code") or "") - current = deduplicated.get(code) - if code and ( - current is None or str(row.get("in_date") or "") > str(current.get("in_date") or "") - ): - deduplicated[code] = row - if not deduplicated: - raise ProviderError("该申万行业没有有效成分股") daily = self._query( "daily", {"trade_date": target}, @@ -170,7 +143,8 @@ class TushareProvider: ) quote_map = {str(row.get("ts_code") or ""): row for row in daily.rows} rows = [] - for code, member in deduplicated.items(): + for member in members: + code = str(member.get("ts_code") or "") quote = quote_map.get(code) or {} rows.append( { @@ -188,6 +162,114 @@ class TushareProvider: coverage = sum(bool(row["quoted"]) for row in rows) / len(rows) return ProviderResult(tuple(rows), _metadata(self.source, "mixed", coverage)) + def heaven_inputs( + self, representative: str, trade_date: str + ) -> dict[str, ProviderResult | None]: + target = _compact(trade_date) + membership = self.sector_members(representative, trade_date) + sector_code = str(membership.rows[0].get("sector_code") or "") if membership.rows else "" + return { + "members": membership, + "daily": self._optional_query( + "daily", + {"trade_date": target}, + "ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount", + ), + "daily_basic": self._optional_query( + "daily_basic", + {"trade_date": target}, + "ts_code,trade_date,turnover_rate,volume_ratio,total_mv,circ_mv", + ), + "sector_daily": self._optional_query( + "sw_daily", + {"ts_code": sector_code, "trade_date": target}, + "ts_code,trade_date,name,open,high,low,close,pct_change,vol,amount,pe,pb,float_mv,total_mv", + ), + "indices": self._index_rows(target), + } + + def heaven_realtime_inputs( + self, representative: str, trade_date: str, previous_trade_date: str + ) -> dict[str, ProviderResult | None]: + target = _compact(trade_date) + previous = _compact(previous_trade_date) + industry, members = self._sector_memberships(representative, target) + sector_code = str(industry.get("l2_code") or "") + directory = self._optional_query( + "stock_basic", + {"exchange": "", "list_status": "L"}, + "ts_code,name,industry,market,list_date", + ) + active_codes = tuple( + str(row.get("ts_code") or "") + for row in (directory.rows if directory else ()) + if row.get("ts_code") + ) + realtime_codes = (*active_codes, "000001.SH", "399001.SZ", "399006.SZ") + member_rows = tuple( + { + "sector_code": sector_code, + "sector_name": str(industry.get("l2_name") or "").strip(), + "ts_code": str(row.get("ts_code") or ""), + "name": str(row.get("name") or "").strip(), + } + for row in members + ) + start = (datetime.strptime(target, "%Y%m%d") - timedelta(days=35)).strftime("%Y%m%d") + return { + "directory": directory, + "members": ProviderResult( + member_rows, + _metadata(self.source, "membership", 1 if member_rows else 0), + ), + "realtime": self._optional_query( + "rt_k", + {"ts_code": ",".join(realtime_codes)}, + "ts_code,name,trade_time,open,high,low,close,pre_close,vol,amount,num,pct_chg", + ), + "capital": self._optional_query( + "daily_basic", + {"trade_date": previous}, + "ts_code,trade_date,total_share,float_share,free_share,total_mv,circ_mv", + ), + "stock_history": self._optional_query( + "daily", + {"ts_code": representative, "start_date": start, "end_date": previous}, + "ts_code,trade_date,vol,amount", + ), + "price_limits": self._optional_query( + "stk_limit", + {"trade_date": target}, + "ts_code,trade_date,up_limit,down_limit", + ), + "suspensions": self._optional_query( + "suspend_d", + {"suspend_date": target}, + "ts_code,suspend_date,resume_date,suspend_timing,suspend_type", + ), + "sector_realtime": self._optional_query( + "rt_sw_k", + {"ts_code": sector_code}, + "ts_code,name,trade_time,close,pre_close,high,open,low,vol,amount,pct_change", + ), + } + + def _index_rows(self, trade_date: str) -> ProviderResult | None: + rows: list[dict[str, Any]] = [] + completed = 0 + for identifier in ("000001.SH", "399001.SZ", "399006.SZ"): + result = self._optional_query( + "index_daily", + {"ts_code": identifier, "trade_date": trade_date}, + "ts_code,trade_date,close,pre_close,pct_chg", + ) + if result is not None and result.rows: + rows.extend(result.rows) + completed += 1 + if not rows: + return None + return ProviderResult(tuple(rows), _metadata(self.source, "percent", completed / 3)) + def market_insight( self, kind: str, @@ -443,6 +525,40 @@ class TushareProvider: rows.extend(result.rows) return rows + def _sector_memberships( + self, representative: str, trade_date: str + ) -> tuple[dict[str, Any], list[dict[str, Any]]]: + active = [ + row + for row in self._membership_rows({"ts_code": representative}) + if _active_on(row, trade_date) + ] + if not active: + raise ProviderError("未找到该股票在目标日期的申万行业") + industry = max( + active, + key=lambda row: (str(row.get("in_date") or ""), str(row.get("l2_code") or "")), + ) + sector_code = str(industry.get("l2_code") or "") + if not sector_code: + raise ProviderError("该股票缺少申万二级行业") + rows = [ + row + for row in self._membership_rows({"l2_code": sector_code}) + if _active_on(row, trade_date) + ] + deduplicated: dict[str, dict[str, Any]] = {} + for row in rows: + code = str(row.get("ts_code") or "") + current = deduplicated.get(code) + if code and ( + current is None or str(row.get("in_date") or "") > str(current.get("in_date") or "") + ): + deduplicated[code] = row + if not deduplicated: + raise ProviderError("该申万行业没有有效成分股") + return industry, list(deduplicated.values()) + def _query( self, api_name: str, diff --git a/next/backend/features/market/sentiment.py b/next/backend/data/sentiment.py similarity index 100% rename from next/backend/features/market/sentiment.py rename to next/backend/data/sentiment.py diff --git a/next/backend/database/migrations/m0009_heaven.py b/next/backend/database/migrations/m0009_heaven.py new file mode 100644 index 0000000..14ae8fe --- /dev/null +++ b/next/backend/database/migrations/m0009_heaven.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import sqlite3 + +from backend.database.migrations.runner import Migration + + +def upgrade(connection: sqlite3.Connection) -> None: + connection.execute( + """ + CREATE TABLE heaven_readings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + mode TEXT NOT NULL CHECK (mode IN ('trend', 'fortune', 'heart')), + reading_date TEXT NOT NULL, + subject_key TEXT NOT NULL DEFAULT '', + result_json TEXT NOT NULL, + interpretation TEXT NOT NULL DEFAULT '', + interpretation_status TEXT NOT NULL DEFAULT 'pending' CHECK ( + interpretation_status IN ('pending', 'complete', 'stopped', 'error') + ), + request_id TEXT REFERENCES llm_requests(id) ON DELETE SET NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + connection.execute( + """ + CREATE INDEX heaven_readings_scope_idx + ON heaven_readings(user_id, mode, reading_date, id) + """ + ) + connection.execute( + """ + CREATE UNIQUE INDEX heaven_fortune_daily_idx + ON heaven_readings(user_id, reading_date) + WHERE mode = 'fortune' + """ + ) + + +def downgrade(connection: sqlite3.Connection) -> None: + connection.execute("DROP TABLE heaven_readings") + + +MIGRATION = Migration( + version=9, + name="create_heaven_readings", + signature="heaven:v2:account-readings-unique-daily-fortune", + upgrade=upgrade, + downgrade=downgrade, +) diff --git a/next/backend/database/migrations/registry.py b/next/backend/database/migrations/registry.py index 2a381a4..6efee63 100644 --- a/next/backend/database/migrations/registry.py +++ b/next/backend/database/migrations/registry.py @@ -6,6 +6,7 @@ from backend.database.migrations.m0005_market_insights import MIGRATION as MARKE from backend.database.migrations.m0006_watchlists import MIGRATION as WATCHLISTS from backend.database.migrations.m0007_screener import MIGRATION as SCREENER from backend.database.migrations.m0008_mentor_llm import MIGRATION as MENTOR_LLM +from backend.database.migrations.m0009_heaven import MIGRATION as HEAVEN from backend.database.migrations.runner import Migration MIGRATIONS: tuple[Migration, ...] = ( @@ -17,4 +18,5 @@ MIGRATIONS: tuple[Migration, ...] = ( WATCHLISTS, SCREENER, MENTOR_LLM, + HEAVEN, ) diff --git a/next/backend/features/heaven/__init__.py b/next/backend/features/heaven/__init__.py new file mode 100644 index 0000000..fc981c0 --- /dev/null +++ b/next/backend/features/heaven/__init__.py @@ -0,0 +1,3 @@ +from backend.features.heaven.service import HeavenService + +__all__ = ["HeavenService"] diff --git a/next/backend/features/heaven/fortune.py b/next/backend/features/heaven/fortune.py new file mode 100644 index 0000000..9304488 --- /dev/null +++ b/next/backend/features/heaven/fortune.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +from datetime import date, datetime +from typing import Any + +from lunar_python import Solar + +ELEMENTS = ("木", "火", "土", "金", "水") +STEM_MOVEMENT = { + "甲": "土", + "己": "土", + "乙": "金", + "庚": "金", + "丙": "水", + "辛": "水", + "丁": "木", + "壬": "木", + "戊": "火", + "癸": "火", +} +STEM_ELEMENT = { + "甲": "木", + "乙": "木", + "丙": "火", + "丁": "火", + "戊": "土", + "己": "土", + "庚": "金", + "辛": "金", + "壬": "水", + "癸": "水", +} +BRANCH_ELEMENT = { + "子": "水", + "丑": "土", + "寅": "木", + "卯": "木", + "辰": "土", + "巳": "火", + "午": "火", + "未": "土", + "申": "金", + "酉": "金", + "戌": "土", + "亥": "水", +} +SITIAN = { + "子": "少阴君火", + "午": "少阴君火", + "丑": "太阴湿土", + "未": "太阴湿土", + "寅": "少阳相火", + "申": "少阳相火", + "卯": "阳明燥金", + "酉": "阳明燥金", + "辰": "太阳寒水", + "戌": "太阳寒水", + "巳": "厥阴风木", + "亥": "厥阴风木", +} +ZAIQUAN = { + "少阴君火": "阳明燥金", + "太阴湿土": "太阳寒水", + "少阳相火": "厥阴风木", + "阳明燥金": "少阴君火", + "太阳寒水": "太阴湿土", + "厥阴风木": "少阳相火", +} +QI_SEQUENCE = ("厥阴风木", "少阴君火", "太阴湿土", "少阳相火", "阳明燥金", "太阳寒水") +HOST_SEQUENCE = ("厥阴风木", "少阴君火", "少阳相火", "太阴湿土", "阳明燥金", "太阳寒水") +QI_ELEMENT = {name: name[-1] for name in QI_SEQUENCE} +STEP_NAMES = ("初之气", "二之气", "三之气", "四之气", "五之气", "终之气") +GENERATES = {"木": "火", "火": "土", "土": "金", "金": "水", "水": "木"} +CONTROLS = {"木": "土", "土": "水", "水": "火", "火": "金", "金": "木"} +CLIMATE = { + "木": "风木疏动", + "火": "热象渐显", + "土": "湿滞偏重", + "金": "燥气收敛", + "水": "寒意潜行", +} +BEHAVIOR = { + "木": ("求新与扩张感增强", "防止把萌芽误作主升", "先写清验证条件"), + "火": ("兴奋与急迫感增强", "防止把一致误作确定", "延迟一次冲动决策"), + "土": ("对确定性的需求增强", "防止把犹豫误作耐心", "按失效条件做减法"), + "金": ("警觉与裁决感增强", "防止过早否定修复", "区分逻辑失效与波动"), + "水": ("避险与不确定感增强", "防止放大最坏想象", "降低频率并保留预案"), +} +INDUSTRIES = { + "木": ("农业", "林业", "医药", "教育", "纺织", "家居"), + "火": ("电力", "新能源", "电子", "半导体", "通信", "传媒"), + "土": ("地产", "建筑", "建材", "食品", "零售", "仓储"), + "金": ("银行", "证券", "保险", "有色", "机械", "军工"), + "水": ("航运", "物流", "水务", "饮料", "化工", "旅游"), +} + + +def build(trade_date: str, profile: Any | None = None) -> dict[str, Any]: + parsed = date.fromisoformat(trade_date) + solar = Solar.fromYmdHms(parsed.year, parsed.month, parsed.day, 12, 0, 0) + lunar = solar.getLunar() + year_gz = lunar.getYearInGanZhiExact() + month_gz = lunar.getMonthInGanZhiExact() + day_gz = lunar.getDayInGanZhiExact() + sitian = SITIAN[year_gz[1]] + zaiquan = ZAIQUAN[sitian] + step = _qi_step(lunar, solar.toYmd()) + host = HOST_SEQUENCE[step - 1] + guest = QI_SEQUENCE[(QI_SEQUENCE.index(sitian) - 2 + step - 1) % 6] + sitian_weight, zaiquan_weight = (15, 5) if step <= 3 else (5, 15) + layers = ( + _layer( + "年纲", + ( + (STEM_MOVEMENT[year_gz[0]], 30), + (QI_ELEMENT[sitian], sitian_weight), + (QI_ELEMENT[zaiquan], zaiquan_weight), + ), + ), + _layer("客主加临", ((QI_ELEMENT[host], 20), (QI_ELEMENT[guest], 25))), + _layer("日辰触发", ((STEM_MOVEMENT[day_gz[0]], 2.5), (BRANCH_ELEMENT[day_gz[1]], 2.5))), + ) + totals = {element: sum(layer["weights"][element] for layer in layers) for element in ELEMENTS} + balance = sorted( + ( + {"element": element, "score": score, "percent": round(score)} + for element, score in totals.items() + ), + key=lambda item: item["score"], + reverse=True, + ) + primary, secondary = balance[0]["element"], balance[1]["element"] + phrase = f"{CLIMATE[primary]}·{CLIMATE[secondary]}" + behavior = BEHAVIOR[primary] + return { + "date": trade_date, + "lunar_date": f"农历{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}", + "pillars": {"year": year_gz, "month": month_gz, "day": day_gz}, + "solar_term": { + "current": lunar.getPrevJieQi().getName(), + "next": lunar.getNextJieQi().getName(), + }, + "phrase": phrase, + "movement": { + "element": STEM_MOVEMENT[year_gz[0]], + "tendency": "太过" if year_gz[0] in "甲丙戊庚壬" else "不及", + }, + "six_qi": { + "sitian": sitian, + "zaiquan": zaiquan, + "step": step, + "step_name": STEP_NAMES[step - 1], + "host": host, + "guest": guest, + }, + "layers": [ + { + "label": layers[0]["label"], + "dominant": layers[0]["dominant"], + "summary": f"{STEM_MOVEMENT[year_gz[0]]}运为纲,司天{sitian},在泉{zaiquan}", + }, + { + "label": layers[1]["label"], + "dominant": layers[1]["dominant"], + "summary": ( + f"客{guest}加临主{host},{_relation(QI_ELEMENT[guest], QI_ELEMENT[host])}" + ), + }, + { + "label": layers[2]["label"], + "dominant": layers[2]["dominant"], + "summary": f"{day_gz}日,日干与日支只作轻量触发", + }, + ], + "balance": balance, + "human_field": { + "emotional_tendency": behavior[0], + "risk": behavior[1], + "balancing_action": behavior[2], + "generation_control": _generation_control(primary, secondary), + }, + "personal": _personal(profile, (primary, secondary)), + "sector_catalog": [ + {"element": element, "industries": list(INDUSTRIES[element])} for element in ELEMENTS + ], + "notice": "五行气场是传统历法与市场行为的象征性观察,不代表可验证的因果关系。", + } + + +def _personal(profile: Any | None, dominant: tuple[str, str]) -> dict[str, Any] | None: + if profile is None: + return None + born = datetime.strptime(f"{profile.birth_date}T{profile.birth_time}", "%Y-%m-%dT%H:%M") + lunar = Solar.fromYmdHms(born.year, born.month, born.day, born.hour, born.minute, 0).getLunar() + eight = lunar.getEightChar() + pillars = (eight.getYear(), eight.getMonth(), eight.getDay(), eight.getTime()) + weights = {element: 0.0 for element in ELEMENTS} + for index, pillar in enumerate(pillars): + weights[STEM_ELEMENT[pillar[0]]] += 1 + weights[BRANCH_ELEMENT[pillar[1]]] += 1.5 if index == 1 else 1 + day_element = STEM_ELEMENT[eight.getDayGan()] + supportive = { + day_element, + next(element for element, target in GENERATES.items() if target == day_element), + } + hits = [element for element in dominant if element in supportive] + return { + "day_master_element": day_element, + "balance": sorted(weights.items(), key=lambda item: item[1], reverse=True), + "tone": f"当日主气中{'、'.join(hits)}较合个人生扶倾向" + if hits + else "当日主气与个人生扶倾向交错,宜先察情绪再行动", + "notice": "个人信息只用于本地派生计算,页面不回显出生日期、时辰和性别。", + } + + +def _layer(label: str, parts: tuple[tuple[str, float], ...]) -> dict[str, Any]: + weights = {element: 0.0 for element in ELEMENTS} + for element, amount in parts: + weights[element] += amount + return {"label": label, "weights": weights, "dominant": max(weights, key=weights.get)} + + +def _qi_step(lunar: Any, ymd: str) -> int: + current = int(ymd.replace("-", "")) + boundaries = [ + int(lunar.getJieQiTable()[name].toYmd().replace("-", "")) + for name in ("大寒", "春分", "小满", "大暑", "秋分", "小雪") + if lunar.getJieQiTable().get(name) is not None + ] + if len(boundaries) != 6 or current < boundaries[0] or current >= boundaries[5]: + return 6 if len(boundaries) == 6 else 1 + return next( + (index + 1 for index in range(5) if boundaries[index] <= current < boundaries[index + 1]), 6 + ) + + +def _relation(guest: str, host: str) -> str: + if guest == host: + return "客主同气" + if GENERATES[guest] == host: + return "客生主,气机相接" + if GENERATES[host] == guest: + return "主生客,时令外泄" + if CONTROLS[guest] == host: + return "客克主,外来变化偏强" + return "主克客,时令与来气相持" + + +def _generation_control(primary: str, secondary: str) -> str: + if GENERATES[primary] == secondary: + return f"{primary}生{secondary},主气向次气流转" + if CONTROLS[primary] == secondary: + return f"{primary}克{secondary},主次之气相制" + if GENERATES[secondary] == primary: + return f"{secondary}生{primary},次气助主" + if CONTROLS[secondary] == primary: + return f"{secondary}克{primary},次气牵制主气" + return f"{primary}{secondary}并见,宜防一端偏盛" diff --git a/next/backend/features/heaven/hexagram.py b/next/backend/features/heaven/hexagram.py new file mode 100644 index 0000000..b80569c --- /dev/null +++ b/next/backend/features/heaven/hexagram.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import json +from functools import lru_cache +from pathlib import Path +from typing import Any + +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 = ("初爻", "二爻", "三爻", "四爻", "五爻", "上爻") + + +def from_lines(values: list[int], data_path: Path) -> 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 = _load(data_path) + primary = data.get(str(bits)) + transformed = data.get(str(transformed_bits)) + if not primary or not transformed: + raise ValueError("卦象数据不完整。") + line_texts = list(primary["lines"].values()) + lines = [ + { + "position": index + 1, + "position_name": LINE_POSITIONS[index], + "value": value, + "yin_yang": "阳" if value % 2 else "阴", + "moving": value in {6, 9}, + "line_name": line_texts[index]["name"], + "text": line_texts[index]["text"], + "image": line_texts[index].get("image") or "", + } + for index, value in enumerate(values) + ] + return { + "name": primary["name"], + "text": primary["text"], + "image": primary.get("image") or "", + "inner_trigram": TRIGRAM_NAMES[bits[:3]], + "outer_trigram": TRIGRAM_NAMES[bits[3:]], + "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": TRIGRAM_NAMES[transformed_bits[:3]], + "outer_trigram": TRIGRAM_NAMES[transformed_bits[3:]], + }, + } + + +def line_for_score(score: float) -> int: + if score >= 0.72: + return 9 + if score >= 0: + return 7 + if score <= -0.72: + return 6 + return 8 + + +@lru_cache(maxsize=2) +def _load(path: Path) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + return payload.get("hexagrams", payload) diff --git a/next/backend/features/heaven/prompt.py b/next/backend/features/heaven/prompt.py new file mode 100644 index 0000000..7fef677 --- /dev/null +++ b/next/backend/features/heaven/prompt.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import json +from typing import Any + +PROMPT_VERSION = "heaven-deterministic-v1" + + +def messages(mode: str, result: dict[str, Any]) -> list[dict[str, str]]: + instructions = { + "trend": "解释既有本卦、动爻、之卦、六爻量化依据和势值,不得另起卦或修改行情。", + "fortune": ( + "解释既有五运六气三层气机、复合断语、个人派生影响、" + "生克断语与制衡动作,不得修改干支历法。" + ), + "heart": "解释既有本卦、动爻、之卦和卦辞,帮助用户观察第一念,不得另起卦或作确定性预测。", + } + safe = _safe_result(mode, result) + return [ + { + "role": "system", + "content": ( + "你是传统文化观察的文字整理助手。" + + instructions[mode] + + "明确说明内容仅供传统文化与娱乐化观察,不构成预测或投资建议。" + ), + }, + {"role": "user", "content": json.dumps(safe, ensure_ascii=False, separators=(",", ":"))}, + ] + + +def _safe_result(mode: str, result: dict[str, Any]) -> dict[str, Any]: + if mode != "fortune": + return result + return {key: value for key, value in result.items() if key != "personal"} | { + "personal_synthesis": (result.get("personal") or {}).get("tone") + } diff --git a/next/backend/features/heaven/repository.py b/next/backend/features/heaven/repository.py new file mode 100644 index 0000000..ca125b2 --- /dev/null +++ b/next/backend/features/heaven/repository.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import json +import sqlite3 +from typing import Any + + +class HeavenRepository: + def add( + self, + connection: sqlite3.Connection, + *, + user_id: int, + mode: str, + reading_date: str, + subject_key: str, + result: dict[str, Any], + created_at: str, + ) -> int: + cursor = connection.execute( + """ + INSERT INTO heaven_readings ( + user_id, mode, reading_date, subject_key, result_json, + interpretation_status, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?) + """, + ( + user_id, + mode, + reading_date, + subject_key, + json.dumps(result, ensure_ascii=False, separators=(",", ":")), + created_at, + created_at, + ), + ) + return int(cursor.lastrowid) + + def get( + self, connection: sqlite3.Connection, user_id: int, reading_id: int + ) -> sqlite3.Row | None: + return connection.execute( + "SELECT * FROM heaven_readings WHERE id = ? AND user_id = ?", + (reading_id, user_id), + ).fetchone() + + def latest_fortune( + self, connection: sqlite3.Connection, user_id: int, reading_date: str + ) -> sqlite3.Row | None: + return connection.execute( + """ + SELECT * FROM heaven_readings + WHERE user_id = ? AND mode = 'fortune' AND reading_date = ? + ORDER BY id DESC LIMIT 1 + """, + (user_id, reading_date), + ).fetchone() + + def ensure_fortune( + self, + connection: sqlite3.Connection, + *, + user_id: int, + reading_date: str, + result: dict[str, Any], + created_at: str, + ) -> tuple[sqlite3.Row, bool]: + cursor = connection.execute( + """ + INSERT OR IGNORE INTO heaven_readings ( + user_id, mode, reading_date, subject_key, result_json, + interpretation_status, created_at, updated_at + ) VALUES (?, 'fortune', ?, '', ?, 'pending', ?, ?) + """, + ( + user_id, + reading_date, + json.dumps(result, ensure_ascii=False, separators=(",", ":")), + created_at, + created_at, + ), + ) + row = self.latest_fortune(connection, user_id, reading_date) + if row is None: + raise RuntimeError("每日解运记录写入失败") + return row, cursor.rowcount == 0 + + def list( + self, + connection: sqlite3.Connection, + user_id: int, + mode: str | None, + reading_date: str | None, + limit: int = 60, + ) -> tuple[sqlite3.Row, ...]: + clauses = ["user_id = ?"] + values: list[Any] = [user_id] + if mode: + clauses.append("mode = ?") + values.append(mode) + if reading_date: + clauses.append("reading_date = ?") + values.append(reading_date) + values.append(limit) + statement = ( + f"SELECT * FROM heaven_readings WHERE {' AND '.join(clauses)} ORDER BY id DESC LIMIT ?" + ) + return tuple( + connection.execute( + statement, + values, + ).fetchall() + ) + + def update_interpretation( + self, + connection: sqlite3.Connection, + reading_id: int, + user_id: int, + content: str, + status: str, + request_id: str | None, + updated_at: str, + ) -> None: + connection.execute( + """ + UPDATE heaven_readings + SET interpretation = ?, interpretation_status = ?, request_id = ?, updated_at = ? + WHERE id = ? AND user_id = ? + """, + (content, status, request_id, updated_at, reading_id, user_id), + ) + + def delete(self, connection: sqlite3.Connection, user_id: int, reading_id: int) -> int: + cursor = connection.execute( + "DELETE FROM heaven_readings WHERE id = ? AND user_id = ?", + (reading_id, user_id), + ) + return cursor.rowcount diff --git a/next/backend/features/heaven/routes.py b/next/backend/features/heaven/routes.py new file mode 100644 index 0000000..1191422 --- /dev/null +++ b/next/backend/features/heaven/routes.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import json +from collections.abc import Iterator +from typing import Annotated, Literal + +from fastapi import APIRouter, Query, Request +from fastapi.responses import StreamingResponse + +from backend.data.gateway import MarketDataUnavailable +from backend.features.accounts.auth import SmartAccessPrincipal, SmartWritePrincipal +from backend.features.heaven.schemas import ( + DeleteResponse, + FortuneInput, + HeartCompleteInput, + HeartLineInput, + InterpretInput, + TrendInput, +) +from backend.features.heaven.service import HeavenError +from backend.http.errors import AppError +from backend.llm.gateway import LLMGatewayError + +router = APIRouter(prefix="/heaven", tags=["heaven"]) + + +@router.get("/setup", response_model=dict) +def setup( + request: Request, + principal: SmartAccessPrincipal, + reading_date: Annotated[str, Query(alias="date", min_length=10, max_length=10)], +) -> dict: + return _call(request, "setup", principal, reading_date) + + +@router.post("/trend/load", response_model=dict) +def load_trend(payload: TrendInput, request: Request, principal: SmartWritePrincipal) -> dict: + return _call(request, "trend", principal, payload.query, payload.trade_date, payload.manual) + + +@router.post("/fortune", response_model=dict) +def create_fortune(payload: FortuneInput, request: Request, principal: SmartWritePrincipal) -> dict: + return _call(request, "fortune", principal, payload.trade_date) + + +@router.post("/heart/line", response_model=dict) +def heart_line(payload: HeartLineInput, request: Request, principal: SmartWritePrincipal) -> dict: + return _call(request, "heart_line", principal, payload.trade_date, payload.values) + + +@router.post("/heart/complete", response_model=dict) +def complete_heart( + payload: HeartCompleteInput, request: Request, principal: SmartWritePrincipal +) -> dict: + return _call( + request, + "complete_heart", + principal, + payload.trade_date, + payload.values, + payload.first_thought_confirmed, + ) + + +@router.get("/readings", response_model=list[dict]) +def readings( + request: Request, + principal: SmartAccessPrincipal, + mode: Annotated[Literal["trend", "fortune", "heart"] | None, Query()] = None, + reading_date: Annotated[str | None, Query(alias="date")] = None, +) -> list[dict]: + return _call(request, "readings", principal, mode, reading_date) + + +@router.delete("/readings/{reading_id}", response_model=DeleteResponse) +def delete_reading( + reading_id: int, request: Request, principal: SmartWritePrincipal +) -> DeleteResponse: + return DeleteResponse(deleted=_call(request, "delete", principal, reading_id)) + + +@router.post("/interpret") +def interpret( + payload: InterpretInput, request: Request, principal: SmartWritePrincipal +) -> StreamingResponse: + prepared = _call(request, "prepare_interpret", principal, payload.reading_id) + + def body() -> Iterator[bytes]: + for event in request.app.state.container.heaven.stream_interpret(prepared): + yield (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode( + "utf-8" + ) + + return StreamingResponse( + body(), + media_type="application/x-ndjson", + headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"}, + ) + + +def _call(request: Request, method: str, *args): + try: + return getattr(request.app.state.container.heaven, method)(*args) + except HeavenError as exc: + raise AppError("heaven_unavailable", str(exc), 409) from exc + except MarketDataUnavailable as exc: + raise AppError("market_data_unavailable", str(exc), 503) from exc + except LLMGatewayError as exc: + status = 403 if exc.code in {"membership_required", "quota_exhausted"} else 503 + raise AppError(exc.code, str(exc), status) from exc diff --git a/next/backend/features/heaven/schemas.py b/next/backend/features/heaven/schemas.py new file mode 100644 index 0000000..e7a4f05 --- /dev/null +++ b/next/backend/features/heaven/schemas.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +class TrendInput(BaseModel): + query: str = Field(min_length=1, max_length=40) + trade_date: str = Field(min_length=10, max_length=10) + manual: dict[str, Any] = Field(default_factory=dict) + + +class FortuneInput(BaseModel): + trade_date: str = Field(min_length=10, max_length=10) + + +class HeartLineInput(BaseModel): + trade_date: str = Field(min_length=10, max_length=10) + values: list[Literal[6, 7, 8, 9]] = Field(default_factory=list, max_length=5) + + +class HeartCompleteInput(BaseModel): + trade_date: str = Field(min_length=10, max_length=10) + values: list[Literal[6, 7, 8, 9]] = Field(min_length=6, max_length=6) + first_thought_confirmed: bool + + +class InterpretInput(BaseModel): + reading_id: int = Field(gt=0) + + +class DeleteResponse(BaseModel): + deleted: int diff --git a/next/backend/features/heaven/service.py b/next/backend/features/heaven/service.py new file mode 100644 index 0000000..b6456e3 --- /dev/null +++ b/next/backend/features/heaven/service.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import json +import secrets +from collections.abc import Iterator +from dataclasses import dataclass +from datetime import date, datetime +from pathlib import Path +from typing import Any +from zoneinfo import ZoneInfo + +from backend.data.gateway import DataGateway +from backend.database.connection import Database +from backend.features.accounts.models import Principal +from backend.features.accounts.service import AccountService +from backend.features.heaven import fortune, trend +from backend.features.heaven.hexagram import from_lines +from backend.features.heaven.prompt import PROMPT_VERSION, messages +from backend.features.heaven.repository import HeavenRepository +from backend.llm.gateway import LLMCall, LLMGateway, LLMGatewayError + +SHANGHAI = ZoneInfo("Asia/Shanghai") + + +class HeavenError(RuntimeError): + pass + + +@dataclass(frozen=True, slots=True) +class PreparedInterpretation: + reading_id: int + user_id: int + mode: str + prompt: list[dict[str, str]] + call: LLMCall | None + cached: str = "" + + +class HeavenService: + def __init__( + self, + database: Database, + repository: HeavenRepository, + gateway: DataGateway, + accounts: AccountService, + llm: LLMGateway, + iching_path: Path, + ) -> None: + self._database = database + self._repository = repository + self._gateway = gateway + self._accounts = accounts + self._llm = llm + self._iching_path = iching_path + + def setup(self, principal: Principal, requested_date: str) -> dict[str, Any]: + reading_date = self._date(requested_date) + field = fortune.build(reading_date, self._accounts.get_profile(principal.user.id)) + with self._database.read() as connection: + daily = self._repository.latest_fortune(connection, principal.user.id, reading_date) + rows = self._repository.list(connection, principal.user.id, None, None, 60) + return { + "date": reading_date, + "fortune": field, + "daily_fortune": _public(daily) if daily else None, + "history": [_public(row) for row in rows], + } + + def trend( + self, + principal: Principal, + query: str, + requested_date: str, + manual: dict[str, Any], + ) -> dict[str, Any]: + reading_date = self._date(requested_date) + payload = self._gateway.heaven_trend_inputs(query, reading_date) + try: + result = trend.calculate(payload, self._iching_path, manual) + except trend.TrendDataError as exc: + return { + "ready": False, + "message": str(exc), + "trade_date": payload.get("trade_date"), + "stock": payload.get("stock"), + "sector": payload.get("sector"), + "checks": exc.checks, + "automatic": exc.payload, + } + reading_id = self._save( + principal.user.id, + "trend", + result["trade_date"], + str(result["stock"]["identifier"]), + result, + ) + return {"ready": True, "reading_id": reading_id, "result": result} + + def fortune(self, principal: Principal, requested_date: str) -> dict[str, Any]: + reading_date = self._date(requested_date) + result = fortune.build(reading_date, self._accounts.get_profile(principal.user.id)) + with self._database.transaction() as connection: + row, reused = self._repository.ensure_fortune( + connection, + user_id=principal.user.id, + reading_date=reading_date, + result=result, + created_at=_now(), + ) + return {"reused": reused, "reading": _public(row)} + + def heart_line( + self, principal: Principal, requested_date: str, values: list[int] + ) -> dict[str, Any]: + self._date(requested_date) + if len(values) >= 6 or any(value not in {6, 7, 8, 9} for value in values): + raise HeavenError("当前起卦进度无效。") + faces = [2 + secrets.randbelow(2) for _ in range(3)] + value = sum(faces) + return { + "position": len(values) + 1, + "value": value, + "faces": ["front" if face == 3 else "back" for face in faces], + "values": [*values, value], + } + + def complete_heart( + self, + principal: Principal, + requested_date: str, + values: list[int], + first_thought_confirmed: bool, + ) -> dict[str, Any]: + reading_date = self._date(requested_date) + if not first_thought_confirmed: + raise HeavenError("请先确认第一念,再进入解卦。") + result = { + "date": reading_date, + "hexagram": from_lines(values, self._iching_path), + "notice": "卦象仅供传统文化与自我观察,不构成预测或投资建议。", + } + reading_id = self._save(principal.user.id, "heart", reading_date, "", result) + return {"reading_id": reading_id, "result": result} + + def readings( + self, principal: Principal, mode: str | None, requested_date: str | None + ) -> list[dict[str, Any]]: + reading_date = self._date(requested_date) if requested_date else None + if mode and mode not in {"trend", "fortune", "heart"}: + raise HeavenError("历史类型无效。") + with self._database.read() as connection: + rows = self._repository.list(connection, principal.user.id, mode, reading_date, 60) + return [_public(row) for row in rows] + + def delete(self, principal: Principal, reading_id: int) -> int: + with self._database.transaction() as connection: + return self._repository.delete(connection, principal.user.id, reading_id) + + def prepare_interpret(self, principal: Principal, reading_id: int) -> PreparedInterpretation: + with self._database.read() as connection: + row = self._repository.get(connection, principal.user.id, reading_id) + if row is None: + raise HeavenError("未找到该问天记录。") + if row["interpretation_status"] == "complete" and row["interpretation"]: + return PreparedInterpretation( + reading_id, + principal.user.id, + str(row["mode"]), + [], + None, + str(row["interpretation"]), + ) + result = json.loads(str(row["result_json"])) + prompt = messages(str(row["mode"]), result) + call = self._llm.prepare( + principal, + feature=f"heaven_{row['mode']}", + prompt_version=PROMPT_VERSION, + business_id=f"heaven:{reading_id}", + input_chars=sum(len(item["content"]) for item in prompt), + ) + return PreparedInterpretation(reading_id, principal.user.id, str(row["mode"]), prompt, call) + + def stream_interpret(self, prepared: PreparedInterpretation) -> Iterator[dict[str, Any]]: + if prepared.cached: + yield {"type": "delta", "content": prepared.cached, "cached": True} + yield {"type": "done", "cached": True} + return + if prepared.call is None: + raise HeavenError("智能解读状态无效。") + answer = "" + saved = False + stream = self._llm.stream(prepared.call, prepared.prompt) + try: + for event in stream: + if event.type == "delta": + answer += event.content + yield { + "type": "delta", + "content": event.content, + "request_id": event.request_id, + } + elif event.type == "done": + self._save_interpretation(prepared, answer, "complete") + saved = True + yield {"type": "done", "request_id": event.request_id} + except GeneratorExit: + stream.close() + if answer and not saved: + self._save_interpretation(prepared, answer, "stopped") + raise + except LLMGatewayError as exc: + if answer: + self._save_interpretation(prepared, answer, "error") + yield {"type": "error", "code": exc.code, "message": str(exc), "partial": exc.partial} + + def _save( + self, user_id: int, mode: str, reading_date: str, subject_key: str, result: dict[str, Any] + ) -> int: + now = _now() + with self._database.transaction() as connection: + return self._repository.add( + connection, + user_id=user_id, + mode=mode, + reading_date=reading_date, + subject_key=subject_key, + result=result, + created_at=now, + ) + + def _save_interpretation( + self, prepared: PreparedInterpretation, answer: str, status: str + ) -> None: + with self._database.transaction() as connection: + self._repository.update_interpretation( + connection, + prepared.reading_id, + prepared.user_id, + answer, + status, + prepared.call.request_id if prepared.call else None, + _now(), + ) + + @staticmethod + def _date(value: str) -> str: + try: + return date.fromisoformat(value).isoformat() + except ValueError as exc: + raise HeavenError("日期格式无效。") from exc + + +def _public(row: Any) -> dict[str, Any]: + return { + "id": int(row["id"]), + "mode": str(row["mode"]), + "date": str(row["reading_date"]), + "subject_key": str(row["subject_key"]), + "result": json.loads(str(row["result_json"])), + "interpretation": str(row["interpretation"]), + "status": str(row["interpretation_status"]), + "created_at": str(row["created_at"]), + } + + +def _now() -> str: + return datetime.now(SHANGHAI).isoformat(timespec="seconds") diff --git a/next/backend/features/heaven/trend.py b/next/backend/features/heaven/trend.py new file mode 100644 index 0000000..26ab7fe --- /dev/null +++ b/next/backend/features/heaven/trend.py @@ -0,0 +1,353 @@ +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path +from typing import Any + +from backend.features.heaven.hexagram import LINE_POSITIONS, from_lines, line_for_score + +LINE_META = ( + ("地", "内", "个股内核"), + ("地", "外", "个股外显"), + ("人", "内", "行业内核"), + ("人", "外", "行业外显"), + ("天", "内", "市场内核"), + ("天", "外", "指数外显"), +) +MANUAL_FIELDS = { + "sector.name", + "sector.change", + "sector.up_count", + "sector.down_count", + "sector.member_count", + "sector.quoted_count", + "sector.coverage", + "sector.member_equal_change", + "sector.relative_turnover", + "sector.leader", + "sector.leading_pct", +} + + +class TrendDataError(RuntimeError): + def __init__(self, checks: list[dict[str, Any]], payload: dict[str, Any]) -> None: + super().__init__("六爻量化数据未全部通过安全门,暂不成卦。") + self.checks = checks + self.payload = payload + + +def calculate( + payload: dict[str, Any], + data_path: Path, + manual: dict[str, Any] | None = None, +) -> dict[str, Any]: + normalized, manual_paths = apply_manual(payload, manual or {}) + checks = validate(normalized, manual_paths) + if any(not item["passed"] for item in checks): + raise TrendDataError(checks, normalized) + scores = _scores(normalized) + values = [line_for_score(item["score"]) for item in scores] + hexagram = from_lines(values, data_path) + for index, line in enumerate(hexagram["lines"]): + talent, layer, role = LINE_META[index] + line.update( + talent=talent, + layer=layer, + role=role, + score=round(scores[index]["score"], 4), + evidence=scores[index]["evidence"], + validation=checks[index], + ) + average = sum(item["score"] for item in scores) / 6 + moving_names = [LINE_POSITIONS[index - 1] for index in hexagram["moving_lines"]] + return { + "trade_date": normalized["trade_date"], + "stock": normalized["stock"], + "sector": normalized["sector"], + "hexagram": hexagram, + "movement": { + "moving_names": moving_names, + "label": ( + f"{'、'.join(moving_names)}动,{hexagram['name']}之{hexagram['transformed']['name']}" + if moving_names + else f"无动爻,守{hexagram['name']}本势" + ), + }, + "momentum_score": round(average * 100), + "momentum_label": _momentum_label(average), + "checks": checks, + "manual_fields": sorted(manual_paths), + "notice": "卦象来自客观行情的固定量化映射,仅供传统文化与娱乐化观察。", + } + + +def apply_manual( + payload: dict[str, Any], manual: dict[str, Any] +) -> tuple[dict[str, Any], set[str]]: + result = deepcopy(payload) + failed_paths = _failed_paths(result) + applied: set[str] = set() + for path, value in _flatten(manual).items(): + if path not in MANUAL_FIELDS or path not in failed_paths or value in (None, ""): + continue + section, key = path.split(".", 1) + result.setdefault(section, {})[key] = value + applied.add(path) + return result, applied + + +def validate(payload: dict[str, Any], manual_paths: set[str] | None = None) -> list[dict[str, Any]]: + manual_paths = manual_paths or set() + trade_date = str(payload.get("trade_date") or "") + stock = payload.get("stock") or {} + sector = payload.get("sector") or {} + market = payload.get("market") or {} + indexes = payload.get("indices") or [] + mode = str(payload.get("mode") or "historical") + stock_fields = ( + ( + "change", + "amount_percentile", + "turnover_rate", + "turnover_relative", + "volume_activity_ratio", + ) + if mode == "intraday" + else ("change", "amount_percentile", "turnover_rate") + ) + stock_ok = ( + str(stock.get("trade_date") or "") == trade_date + and str(stock.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily") + and _has(stock, *stock_fields) + ) + sector_common = ( + str(sector.get("trade_date") or trade_date) == trade_date + and str(sector.get("taxonomy") or "") == "申万二级" + and _has( + sector, + "change", + "up_count", + "down_count", + "member_count", + "quoted_count", + "coverage", + "leading_pct", + ) + ) + member_count = _number(sector.get("member_count")) + quoted_count = _number(sector.get("quoted_count")) + coverage = _number(sector.get("coverage")) + complete_members = member_count > 0 and quoted_count == member_count and coverage >= 0.98 + sufficient_members = ( + member_count > 0 and coverage >= 0.98 and quoted_count >= member_count * 0.98 + ) + sector_mode = str(sector.get("quote_kind") or "") == ( + "realtime" if mode == "intraday" else "daily" + ) + if mode == "intraday": + sector_inner = sector_common and sector_mode and _has(sector, "relative_turnover") + else: + sector_inner = sector_common and sector_mode and _has(sector, "member_equal_change") + sector_inner = sector_inner and (complete_members or sufficient_members) + sector_outer = sector_common and sector_mode and bool(str(sector.get("name") or "")) + market_ok = ( + str(market.get("trade_date") or "") == trade_date + and str(market.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily") + and _has( + market, + "sentiment_score", + "seal_rate", + "amount_billion", + "average_amount_billion", + "up_count", + "down_count", + "limit_up_count", + "limit_down_count", + ) + ) + expected = {"000001.SH", "399001.SZ", "399006.SZ"} + present = { + str(item.get("identifier") or "") + for item in indexes + if str(item.get("trade_date") or "") == trade_date + and str(item.get("quote_kind") or "") == ("realtime" if mode == "intraday" else "daily") + and item.get("change") is not None + } + details = ( + (stock_ok, "个股交易日、行情类型及成交活跃数据有效", {"stock"}), + (stock_ok and _has(stock, "streak", "status"), "个股涨跌、连板和事件状态有效", {"stock"}), + (sector_inner, f"申万二级行业有效成分 {int(quoted_count)}/{int(member_count)}", {"sector"}), + (sector_outer, "申万二级行业及领涨股涨跌有效", {"sector"}), + (market_ok, "市场情绪、封板、成交、宽度和涨跌停结构有效", {"market"}), + (present == expected, "上证、深证、创业板三条指数行情完整", {"indices"}), + ) + checks = [] + for index, (passed, message, sections) in enumerate(details): + used_manual = any(path.split(".", 1)[0] in sections for path in manual_paths) + checks.append( + { + "position": index + 1, + "position_name": LINE_POSITIONS[index], + "role": LINE_META[index][2], + "passed": bool(passed), + "source": "manual" if used_manual else "automatic", + "message": message if passed else _failure_message(index, payload), + } + ) + return checks + + +def _scores(payload: dict[str, Any]) -> list[dict[str, Any]]: + stock = payload["stock"] + sector = payload["sector"] + market = payload["market"] + mode = payload.get("mode") or "historical" + amount = _clamp(_number(stock["amount_percentile"]) / 100, 0, 1) + if mode == "intraday": + relative_turnover = _clamp((_number(stock["turnover_relative"]) - 1) / 1.5) + activity = _clamp((_number(stock["volume_activity_ratio"]) - 1) / 1.5) + stock_inner = (amount * 2 - 1) * 0.35 + relative_turnover * 0.35 + activity * 0.30 + else: + turnover = _clamp(_number(stock["turnover_rate"]) / 20, 0, 1) + seal = _clamp(_number(stock.get("seal_amount_million")) / 15000, 0, 1) + stability = 1 - _clamp(_number(stock.get("open_times")) / 6, 0, 1) + stock_inner = (amount * 0.32 + turnover * 0.22 + seal * 0.25 + stability * 0.21) * 2 - 1 + adjustment = -0.7 if stock["status"] == "跌停" else -0.25 if stock["status"] == "炸板" else 0.15 + stock_outer = _clamp( + _clamp(_number(stock["change"]) / 10) * 0.7 + + _clamp(_number(stock["streak"]) / 5, 0, 1) * 0.2 + + adjustment + ) + up = _number(sector["up_count"]) + down = _number(sector["down_count"]) + breadth = _clamp((up - down) / max(up + down, 1)) + leader = _clamp(_number(sector["leading_pct"]) / 10) + if mode == "intraday": + relative = _clamp((_number(sector["relative_turnover"]) - 1) / 1.5) + sector_inner = breadth * 0.6 + relative * 0.4 + else: + equal_change = _clamp(_number(sector["member_equal_change"]) / 5) + sector_inner = breadth * 0.6 + equal_change * 0.35 + leader * 0.05 + sector_outer = _clamp(_number(sector["change"]) / 5) * 0.9 + leader * 0.1 + sentiment = _clamp(_number(market["sentiment_score"]) / 100, 0, 1) * 2 - 1 + seal_rate = _clamp(_number(market["seal_rate"]) / 100, 0, 1) * 2 - 1 + amount_change = _clamp( + (_number(market["amount_billion"]) / max(_number(market["average_amount_billion"]), 1) - 1) + * 3 + ) + market_up, market_down = _number(market["up_count"]), _number(market["down_count"]) + market_breadth = _clamp((market_up / max(market_up + market_down, 1) - 0.5) * 2) + limit_up = _number(market["limit_up_count"]) + limit_down = _number(market["limit_down_count"]) + limit_balance = _clamp((limit_up - limit_down) / max(limit_up + limit_down, 1)) + market_inner = ( + sentiment * 0.35 + + seal_rate * 0.20 + + amount_change * 0.20 + + market_breadth * 0.15 + + limit_balance * 0.10 + ) + index_change = sum(_number(item["change"]) for item in payload["indices"]) / 3 + return [ + { + "score": _clamp(stock_inner), + "evidence": ( + [ + f"成交额分位 {amount * 100:.0f}%", + f"相对换手 {_number(stock['turnover_relative']):.2f}", + f"同进度量能 {_number(stock['volume_activity_ratio']):.2f}", + ] + if mode == "intraday" + else [ + f"成交额分位 {amount * 100:.0f}%", + f"换手率 {_number(stock['turnover_rate']):.2f}%", + ] + ), + }, + { + "score": _clamp(stock_outer), + "evidence": [ + f"涨跌 {_number(stock['change']):+.2f}%", + f"状态 {stock['status'] or '普通'}", + ], + }, + { + "score": _clamp(sector_inner), + "evidence": [ + f"上涨 {int(up)} / 下跌 {int(down)}", + "有效成分 " + f"{int(_number(sector['quoted_count']))}/" + f"{int(_number(sector['member_count']))}", + ], + }, + { + "score": _clamp(sector_outer), + "evidence": [ + f"行业涨跌 {_number(sector['change']):+.2f}%", + f"领涨股 {_number(sector['leading_pct']):+.2f}%", + ], + }, + { + "score": _clamp(market_inner), + "evidence": [ + f"情绪 {_number(market['sentiment_score']):.0f}", + f"封板率 {_number(market['seal_rate']):.1f}%", + ], + }, + {"score": _clamp(index_change / 3), "evidence": [f"三大指数平均 {index_change:+.2f}%"]}, + ] + + +def _failed_paths(payload: dict[str, Any]) -> set[str]: + sector = payload.get("sector") or {} + return {path for path in MANUAL_FIELDS if sector.get(path.split(".", 1)[1]) in (None, "")} + + +def _flatten(value: dict[str, Any], prefix: str = "") -> dict[str, Any]: + result: dict[str, Any] = {} + for key, item in value.items(): + path = f"{prefix}.{key}" if prefix else key + if isinstance(item, dict): + result.update(_flatten(item, path)) + else: + result[path] = item + return result + + +def _has(value: dict[str, Any], *keys: str) -> bool: + return all(key in value and value[key] is not None and value[key] != "" for key in keys) + + +def _failure_message(index: int, payload: dict[str, Any]) -> str: + messages = ( + "个股成交活跃或交易日期数据缺失", + "个股涨跌、连板或事件状态缺失", + "申万二级行业成分宽度、覆盖率或换手数据缺失", + "申万二级行业涨跌或领涨股涨跌缺失", + "市场情绪、成交或宽度数据缺失", + "指数层缺少三大指数的有效行情", + ) + return messages[index] + + +def _clamp(value: float, lower: float = -1, upper: float = 1) -> float: + return max(lower, min(upper, value)) + + +def _number(value: Any) -> float: + try: + return float(value) + except (TypeError, ValueError): + return 0.0 + + +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 "势衰宜守" diff --git a/next/backend/features/market/sync.py b/next/backend/features/market/sync.py index 70b852a..98aceae 100644 --- a/next/backend/features/market/sync.py +++ b/next/backend/features/market/sync.py @@ -9,8 +9,8 @@ from backend.data.contracts import ProviderResult, SnapshotState from backend.data.gateway import DataGateway, MarketDataUnavailable from backend.data.providers.base import ProviderError from backend.data.repository import MarketRepository +from backend.data.sentiment import calculate_sentiment from backend.database.connection import Database -from backend.features.market.sentiment import calculate_sentiment from backend.features.market.snapshot import build_snapshot SHANGHAI = ZoneInfo("Asia/Shanghai") diff --git a/next/backend/http/router.py b/next/backend/http/router.py index 4cce4a8..c44d1cf 100644 --- a/next/backend/http/router.py +++ b/next/backend/http/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from backend.features.accounts.routes import router as accounts_router +from backend.features.heaven.routes import router as heaven_router from backend.features.market.routes import router as market_router from backend.features.mentor.routes import router as mentor_router from backend.features.screener.routes import router as screener_router @@ -12,3 +13,4 @@ api_router.include_router(accounts_router) api_router.include_router(market_router) api_router.include_router(screener_router) api_router.include_router(mentor_router) +api_router.include_router(heaven_router) diff --git a/next/config/heaven/iching_zh.json b/next/config/heaven/iching_zh.json new file mode 100644 index 0000000..cf7f468 --- /dev/null +++ b/next/config/heaven/iching_zh.json @@ -0,0 +1,2393 @@ +{ + "hexagrams": { + "(1, 1, 1, 1, 1, 1)": { + "name": "乾", + "text": "元亨,利贞。", + "image": "天行健,君子以自强不息。", + "lines": { + "1": { + "name": "初九", + "text": "潜龙勿用。", + "image": "潜龙勿用,阳在下也。" + }, + "2": { + "name": "九二", + "text": "见龙在田,利见大人。", + "image": "见龙在田,德施普也。" + }, + "3": { + "name": "九三", + "text": "君子终日乾乾,夕惕若厉,无咎。", + "image": "终日乾乾,反复道也。" + }, + "4": { + "name": "九四", + "text": "或跃在渊,无咎。", + "image": "或跃在渊,进无咎也。" + }, + "5": { + "name": "九五", + "text": "飞龙在天,利见大人。", + "image": "飞龙在天,大人造也。" + }, + "6": { + "name": "上九", + "text": "亢龙有悔。", + "image": "亢龙有悔,盈不可久也。" + } + }, + "use": { + "name": "用九", + "text": "见群龙无首,吉。", + "image": "天德不可为首也。" + } + }, + "(0, 0, 0, 0, 0, 0)": { + "name": "坤", + "text": "元亨,利牝马之贞。君子有攸往,先迷后得主,利西南得朋,东北丧朋。安贞,吉。", + "image": "地势坤,君子以厚德载物。", + "lines": { + "1": { + "name": "初六", + "text": "履霜,坚冰至。", + "image": "履霜坚冰至,阴始凝也。" + }, + "2": { + "name": "六二", + "text": "直,方,大,不习无不利。", + "image": "直方大不习,中正无邪也。" + }, + "3": { + "name": "六三", + "text": "含章可贞。或从王事,无成有终。", + "image": "含章可贞,以时发也。或从王事,知光大也。" + }, + "4": { + "name": "六四", + "text": "括囊;无咎,无誉。", + "image": "括囊无咎,慎不害也。无誉,未受命也。" + }, + "5": { + "name": "六五", + "text": "黄裳,元吉。", + "image": "黄裳元吉,文在中也。" + }, + "6": { + "name": "上六", + "text": "龙战于野,其血玄黄。", + "image": "龙战于野,其道穷也。" + } + }, + "use": { + "name": "用六", + "text": "利永贞。", + "image": "用六永贞,以大终也。" + } + }, + "(1, 0, 0, 0, 1, 0)": { + "name": "屯", + "text": "元亨,利贞,勿用有攸往,利建侯。", + "image": "云雷屯;君子以经纶。", + "lines": { + "1": { + "name": "初九", + "text": "磐桓,利居贞,利建侯。", + "image": "虽磐桓,志行正也。以贵下贱,大得民也。" + }, + "2": { + "name": "六二", + "text": "屯如邅如,乘马班如。匪寇婚媾,女子贞不字,十年乃字。", + "image": "六二之难,乘刚也。十年乃字,反常也。" + }, + "3": { + "name": "六三", + "text": "即鹿无虞,惟入于林中,君子几不如舍,往吝。", + "image": "即鹿无虞,以纵禽也。君子舍之,往吝穷也。" + }, + "4": { + "name": "九四", + "text": "乘马班如,求婚媾,往吉,无不利。", + "image": "求而往,明也。" + }, + "5": { + "name": "九五", + "text": "屯其膏,小贞吉,大贞凶。", + "image": "屯其膏,施未光也。" + }, + "6": { + "name": "上六", + "text": "乘马班如,泣血涟如。", + "image": "泣血涟如,何可长也。" + } + } + }, + "(0, 1, 0, 0, 0, 1)": { + "name": "蒙", + "text": "亨。匪我求童蒙,童蒙求我。初筮告,再三渎,渎则不告。利贞。", + "image": "山下出泉,蒙;君子以果行育德。", + "lines": { + "1": { + "name": "初六", + "text": "发蒙,利用刑人,用说桎梏,以往吝。", + "image": "利用刑人,以正法也。" + }, + "2": { + "name": "九二", + "text": "包蒙,吉。纳妇,吉。子克家。", + "image": "子克家,刚柔接也。" + }, + "3": { + "name": "六三", + "text": "勿用取女,见金夫,不有躬,无攸利。", + "image": "勿用取女,行不顺也。" + }, + "4": { + "name": "六四", + "text": "困蒙,吝。", + "image": "困蒙之吝,独远实也。" + }, + "5": { + "name": "六五", + "text": "童蒙,吉。", + "image": "童蒙之吉,顺以巽也。" + }, + "6": { + "name": "上九", + "text": "击蒙,不利为寇,利御寇。", + "image": "利用御寇,上下顺也。" + } + } + }, + "(1, 1, 1, 0, 1, 0)": { + "name": "需", + "text": "有孚,光亨,贞吉。利涉大川。", + "image": "云上于天,需;君子以饮食宴乐。", + "lines": { + "1": { + "name": "初九", + "text": "需于郊,利用恒,无咎。", + "image": "需于郊,不犯难行也。利用恒,无咎;未失常也。" + }, + "2": { + "name": "九二", + "text": "需于沙,小有言,终吉。", + "image": "需于沙,衍在中也。虽小有言,以终吉也。" + }, + "3": { + "name": "九三", + "text": "需于泥,致寇至。", + "image": "需于泥,灾在外也。自我致寇,敬慎不败也。" + }, + "4": { + "name": "六四", + "text": "需于血,出自穴。", + "image": "需于血,顺以听也。" + }, + "5": { + "name": "九五", + "text": "需于酒食,贞吉。", + "image": "酒食贞吉,以中正也。" + }, + "6": { + "name": "上六", + "text": "入于穴,有不速之客三人来,敬之终吉。", + "image": "不速之客来,敬之终吉;虽不当位,未大失也。" + } + } + }, + "(0, 1, 0, 1, 1, 1)": { + "name": "讼", + "text": "有孚,窒。惕中吉。终凶。利见大人,不利涉大川。", + "image": "天与水违行,讼;君子以作事谋始。", + "lines": { + "1": { + "name": "初六", + "text": "不永所事,小有言,终吉。", + "image": "不永所事,讼不可长也。虽有小言,其辩明也。" + }, + "2": { + "name": "九二", + "text": "不克讼,归而逋,其邑人三百户,无眚。", + "image": "不克讼,归而逋也。自下讼上,患至掇也。" + }, + "3": { + "name": "六三", + "text": "食旧德,贞厉,终吉,或从王事,无成。", + "image": "食旧德,从上吉也。" + }, + "4": { + "name": "九四", + "text": "不克讼,复即命渝,安贞吉。", + "image": "复即命,渝安贞;不失也。" + }, + "5": { + "name": "九五", + "text": "讼,元吉。", + "image": "讼元吉,以中正也。" + }, + "6": { + "name": "上九", + "text": "或锡之鞶带,终朝三褫之。", + "image": "以讼受服,亦不足敬也。" + } + } + }, + "(0, 1, 0, 0, 0, 0)": { + "name": "师", + "text": "贞,丈人吉,无咎。", + "image": "地中有水,师;君子以容民畜众。", + "lines": { + "1": { + "name": "初六", + "text": "师出以律,否臧凶。", + "image": "师出以律,失律凶也。" + }, + "2": { + "name": "九二", + "text": "在师中吉,无咎,王三锡命。", + "image": "在师中吉,承天宠也。王三锡命,怀万邦也。" + }, + "3": { + "name": "六三", + "text": "师或舆尸,凶。", + "image": "师或舆尸,大无功也。" + }, + "4": { + "name": "六四", + "text": "师左次,无咎。", + "image": "左次无咎,未失常也。" + }, + "5": { + "name": "六五", + "text": "田有禽,利执言,无咎。长子帅师,弟子舆尸,贞凶。", + "image": "长子帅师,以中行也。弟子舆尸,使不当也。" + }, + "6": { + "name": "上六", + "text": "大君有命,开国承家,小人勿用。", + "image": "大君有命,以正功也。小人勿用,必乱邦也。" + } + } + }, + "(0, 0, 0, 0, 1, 0)": { + "name": "比", + "text": "吉。原筮元永贞,无咎。不宁方来,后夫凶。", + "image": "地上有水,比;先王以建万国,亲诸侯。", + "lines": { + "1": { + "name": "初六", + "text": "有孚,比之,无咎。有孚盈缶,终来有他,吉。", + "image": "比之初六,有他吉也。" + }, + "2": { + "name": "六二", + "text": "比之自内,贞吉。", + "image": "比之自内,不自失也。" + }, + "3": { + "name": "六三", + "text": "比之匪人。", + "image": "比之匪人,不亦伤乎!" + }, + "4": { + "name": "六四", + "text": "外比之,贞吉。", + "image": "外比於贤,以从上也。" + }, + "5": { + "name": "九五", + "text": "显比,王用三驱,失前禽。邑人不诫,吉。", + "image": "显比之吉,位正中也。舍逆取顺,失前禽也。邑人不诫,上使中也。" + }, + "6": { + "name": "上六", + "text": "比之无首,凶。", + "image": "比之无首,无所终也。" + } + } + }, + "(1, 1, 1, 0, 1, 1)": { + "name": "小畜", + "text": "亨。密云不雨,自我西郊。", + "image": "风行天上,小畜;君子以懿文德。", + "lines": { + "1": { + "name": "初九", + "text": "复自道,何其咎?吉。", + "image": "复自道,其义吉也。" + }, + "2": { + "name": "九二", + "text": "牵复,吉。", + "image": "牵复在中,亦不自失也。" + }, + "3": { + "name": "九三", + "text": "舆说辐,夫妻反目。", + "image": "夫妻反目,不能正室也。" + }, + "4": { + "name": "六四", + "text": "有孚,血去惕出,无咎。", + "image": "有孚惕出,上合志也。" + }, + "5": { + "name": "九五", + "text": "有孚挛如,富以其邻。", + "image": "有孚挛如,不独富也。" + }, + "6": { + "name": "上九", + "text": "既雨既处,尚德载,妇贞厉。月几望,君子征凶。", + "image": "既雨既处,德积载也。君子征凶,有所疑也。" + } + } + }, + "(1, 1, 0, 1, 1, 1)": { + "name": "履", + "text": "履虎尾,不咥人,亨。", + "image": "上天下泽,履;君子以辨上下,安民志。", + "lines": { + "1": { + "name": "初九", + "text": "素履,往,无咎。", + "image": "素履之往,独行愿也。" + }, + "2": { + "name": "九二", + "text": "履道坦坦,幽人贞吉。", + "image": "幽人贞吉,中不自乱也。" + }, + "3": { + "name": "六三", + "text": "眇能视,跛能履,履虎尾,咥人,凶。武人为于大君。", + "image": "眇能视,不足以有明也。跛能履,不足以与行也。咥人之凶,位不当也。武人为于大君,志刚也。" + }, + "4": { + "name": "九四", + "text": "履虎尾,愬愬,终吉。", + "image": "诉诉终吉,志行也。" + }, + "5": { + "name": "九五", + "text": "夬履,贞厉。", + "image": "夬履贞厉,位正当也。" + }, + "6": { + "name": "上九", + "text": "视履考祥,其旋元吉。", + "image": "元吉在上,大有庆也。" + } + } + }, + "(1, 1, 1, 0, 0, 0)": { + "name": "泰", + "text": "小往大来,吉亨。", + "image": "天地交,泰;后以财成天地之道,辅相天地之宜,以左右民。", + "lines": { + "1": { + "name": "初九", + "text": "拔茅茹,以其汇,征吉。", + "image": "拔茅征吉,志在外也。" + }, + "2": { + "name": "九二", + "text": "包荒,用冯河,不遐遗,朋亡,得尚于中行。", + "image": "包荒,得尚于中行,以光大也。" + }, + "3": { + "name": "九三", + "text": "无平不陂,无往不复,艰贞无咎。勿恤其孚,于食有福。", + "image": "无往不复,天地际也。" + }, + "4": { + "name": "六四", + "text": "翩翩,不富,以其邻,不戒以孚。", + "image": "翩翩不富,皆失实也。不戒以孚,中心愿也。" + }, + "5": { + "name": "六五", + "text": "帝乙归妹,以祉元吉。", + "image": "以祉元吉,中以行愿也。" + }, + "6": { + "name": "上六", + "text": "城复于隍,勿用师。自邑告命,贞吝。", + "image": "城复于隍,其命乱也。" + } + } + }, + "(0, 0, 0, 1, 1, 1)": { + "name": "否", + "text": "否之匪人,不利君子贞,大往小来。", + "image": "天地不交,否;君子以俭德辟难,不可荣以禄。", + "lines": { + "1": { + "name": "初六", + "text": "拔茅茹,以其汇,贞吉亨。", + "image": "拔茅贞吉,志在君也。" + }, + "2": { + "name": "六二", + "text": "包承,小人吉,大人否,亨。", + "image": "大人否亨,不乱群也。" + }, + "3": { + "name": "六三", + "text": "包羞。", + "image": "包羞,位不当也。" + }, + "4": { + "name": "九四", + "text": "有命,无咎,畴离祉。", + "image": "有命无咎,志行也。" + }, + "5": { + "name": "九五", + "text": "休否,大人吉。其亡其亡,系于苞桑。", + "image": "大人之吉,位正当也。" + }, + "6": { + "name": "上九", + "text": "倾否,先否后喜。", + "image": "否终则倾,何可长也。" + } + } + }, + "(1, 0, 1, 1, 1, 1)": { + "name": "同人", + "text": "同人于野,亨。利涉大川。利君子贞。", + "image": "天与火,同人;君子以类族辨物。", + "lines": { + "1": { + "name": "初九", + "text": "同人于门,无咎。", + "image": "出门同人,又谁咎也。" + }, + "2": { + "name": "六二", + "text": "同人于宗,吝。", + "image": "同人于宗,吝道也。" + }, + "3": { + "name": "九三", + "text": "伏戎于莽,升其高陵,三岁不兴。", + "image": "伏戎于莽,敌刚也。三岁不兴,安行也。" + }, + "4": { + "name": "九四", + "text": "乘其墉,弗克攻,吉。", + "image": "乘其墉,义弗克也,其吉,则困而反则也。" + }, + "5": { + "name": "九五", + "text": "同人,先号啕而后笑。大师克相遇。", + "image": "同人之先,以中直也。大师相遇,言相克也。" + }, + "6": { + "name": "上九", + "text": "同人于郊,无悔。", + "image": "同人于郊,志未得也。" + } + } + }, + "(1, 1, 1, 1, 0, 1)": { + "name": "大有", + "text": "元亨。", + "image": "火在天上,大有;君子以竭恶扬善,顺天休命。", + "lines": { + "1": { + "name": "初九", + "text": "无交害,匪咎,艰则无咎。", + "image": "大有初九,无交害也。" + }, + "2": { + "name": "九二", + "text": "大车以载,有攸往,无咎。", + "image": "大车以载,积中不败也。" + }, + "3": { + "name": "九三", + "text": "公用亨于天子,小人弗克。", + "image": "公用亨于天子,小人害也。" + }, + "4": { + "name": "九四", + "text": "匪其彭,无咎。", + "image": "匪其彭,无咎;明辨晰也。" + }, + "5": { + "name": "六五", + "text": "厥孚交如,威如,吉。", + "image": "厥孚交如,信以发志也。威如之吉,易而无备也。" + }, + "6": { + "name": "上九", + "text": "自天佑之,吉无不利。", + "image": "大有上吉,自天佑也。" + } + } + }, + "(0, 0, 1, 0, 0, 0)": { + "name": "谦", + "text": "亨。君子有终。", + "image": "地中有山,谦;君子以裒多益寡,称物平施。", + "lines": { + "1": { + "name": "初六", + "text": "谦谦君子,用涉大川,吉。", + "image": "谦谦君子,卑以自牧也。" + }, + "2": { + "name": "六二", + "text": "鸣谦,贞吉。", + "image": "鸣谦贞吉,中心得也。" + }, + "3": { + "name": "九三", + "text": "劳谦君子,有终吉。", + "image": "劳谦君子,万民服也。" + }, + "4": { + "name": "六四", + "text": "无不利,撝谦。", + "image": "无不利,撝谦;不违则也。" + }, + "5": { + "name": "六五", + "text": "不富,以其邻,利用侵伐,无不利。", + "image": "利用侵伐,征不服也。" + }, + "6": { + "name": "上六", + "text": "鸣谦,利用行师,征邑国。", + "image": "鸣谦,志未得也。可用行师,征邑国也。" + } + } + }, + "(0, 0, 0, 1, 0, 0)": { + "name": "豫", + "text": "利建侯行师。", + "image": "雷出地奋,豫。先王以作乐崇德,殷荐之上帝,以配祖考。", + "lines": { + "1": { + "name": "初六", + "text": "鸣豫,凶。", + "image": "初六鸣豫,志穷凶也。" + }, + "2": { + "name": "六二", + "text": "介于石,不终日,贞吉。", + "image": "不终日,贞吉;以中正也。" + }, + "3": { + "name": "六三", + "text": "盱豫,悔。迟有悔。", + "image": "盱豫有悔,位不当也。" + }, + "4": { + "name": "九四", + "text": "由豫,大有得。勿疑。朋盍簪。", + "image": "由豫,大有得;志大行也。" + }, + "5": { + "name": "六五", + "text": "贞疾,恒不死。", + "image": "六五贞疾,乘刚也。恒不死,中未亡也。" + }, + "6": { + "name": "上六", + "text": "冥豫,成有渝,无咎。", + "image": "冥豫在上,何可长也。" + } + } + }, + "(1, 0, 0, 1, 1, 0)": { + "name": "随", + "text": "元亨利贞,无咎。", + "image": "泽中有雷,随;君子以向晦入宴息。", + "lines": { + "1": { + "name": "初六", + "text": "官有渝,贞吉。出门交有功。", + "image": "官有渝,从正吉也。出门交有功,不失也。" + }, + "2": { + "name": "六二", + "text": "系小子,失丈夫。", + "image": "系小子,弗兼与也。" + }, + "3": { + "name": "六三", + "text": "系丈夫,失小子。随有求得,利居贞。", + "image": "系丈夫,志舍下也。" + }, + "4": { + "name": "九四", + "text": "随有获,贞凶。有孚在道,以明,何咎。", + "image": "随有获,其义凶也。有孚在道,明功也。" + }, + "5": { + "name": "九五", + "text": "孚于嘉,吉。", + "image": "孚于嘉,吉;位正中也。" + }, + "6": { + "name": "上六", + "text": "拘系之,乃从维之,王用亨于西山。", + "image": "拘系之,上穷也。" + } + } + }, + "(0, 1, 1, 0, 0, 1)": { + "name": "蛊", + "text": "元亨,利涉大川。先甲三日,后甲三日。", + "image": "山下有风,蛊;君子以振民育德。", + "lines": { + "1": { + "name": "初六", + "text": "干父之蛊,有子,考无咎,厉终吉。", + "image": "干父之蛊,意承考也。" + }, + "2": { + "name": "九二", + "text": "干母之蛊,不可贞。", + "image": "干母之蛊,得中道也。" + }, + "3": { + "name": "九三", + "text": "干父之蛊,小有悔,无大咎。", + "image": "干父之蛊,终无咎也。" + }, + "4": { + "name": "六四", + "text": "裕父之蛊,往见吝。", + "image": "裕父之蛊,往未得也。" + }, + "5": { + "name": "六五", + "text": "干父之蛊,用誉。", + "image": "干父之蛊;承以德也。" + }, + "6": { + "name": "上九", + "text": "不事王侯,高尚其事。", + "image": "不事王侯,志可则也。" + } + } + }, + "(1, 1, 0, 0, 0, 0)": { + "name": "临", + "text": "元,亨,利,贞。至于八月有凶。", + "image": "泽上有地,临;君子以教思无穷,容保民无疆。", + "lines": { + "1": { + "name": "初六", + "text": "咸临,贞吉。", + "image": "咸临贞吉,志行正也。" + }, + "2": { + "name": "九二", + "text": "咸临,吉无不利。", + "image": "咸临,吉无不利;未顺命也。" + }, + "3": { + "name": "六三", + "text": "甘临,无攸利。既忧之,无咎。", + "image": "甘临,位不当也。既忧之,咎不长也。" + }, + "4": { + "name": "六四", + "text": "至临,无咎。", + "image": "至临无咎,位当也。" + }, + "5": { + "name": "六五", + "text": "知临,大君之宜,吉。", + "image": "大君之宜,行中之谓也。" + }, + "6": { + "name": "上六", + "text": "敦临,吉无咎。", + "image": "敦临之吉,志在内也。" + } + } + }, + "(0, 0, 0, 0, 1, 1)": { + "name": "观", + "text": "盥而不荐,有孚顒若。", + "image": "风行地上,观;先王以省方,观民设教。", + "lines": { + "1": { + "name": "初六", + "text": "童观,小人无咎,君子吝。", + "image": "初六童观,小人道也。" + }, + "2": { + "name": "六二", + "text": "窥观,利女贞。", + "image": "窥观女贞,亦可丑也。" + }, + "3": { + "name": "六三", + "text": "观我生,进退。", + "image": "观我生,进退;未失道也。" + }, + "4": { + "name": "六四", + "text": "观国之光,利用宾于王。", + "image": "观国之光,尚宾也。" + }, + "5": { + "name": "九五", + "text": "观我生,君子无咎。", + "image": "观我生,观民也。" + }, + "6": { + "name": "上九", + "text": "观其生,君子无咎。", + "image": "观其生,志未平也。" + } + } + }, + "(1, 0, 0, 1, 0, 1)": { + "name": "噬嗑", + "text": "亨。利用狱。", + "image": "雷电噬嗑;先王以明罚敕法。", + "lines": { + "1": { + "name": "初九", + "text": "屦校灭趾,无咎。", + "image": "屦校灭趾,不行也。" + }, + "2": { + "name": "六二", + "text": "噬肤灭鼻,无咎。", + "image": "噬肤灭鼻,乘刚也。" + }, + "3": { + "name": "六三", + "text": "噬腊肉,遇毒;小吝,无咎。", + "image": "遇毒,位不当也。" + }, + "4": { + "name": "九四", + "text": "噬乾胏,得金矢,利艰贞,吉。", + "image": "利艰贞吉,未光也。" + }, + "5": { + "name": "六五", + "text": "噬乾肉,得黄金,贞厉,无咎。", + "image": "贞厉无咎,得当也。" + }, + "6": { + "name": "上九", + "text": "何校灭耳,凶。", + "image": "何校灭耳,聪不明也。" + } + } + }, + "(1, 0, 1, 0, 0, 1)": { + "name": "贲", + "text": "亨。小利有攸往。", + "image": "山下有火,贲;君子以明庶政,无敢折狱。", + "lines": { + "1": { + "name": "初九", + "text": "贲其趾,舍车而徒。", + "image": "舍车而徒,义弗乘也。" + }, + "2": { + "name": "六二", + "text": "贲其须。", + "image": "贲其须,与上兴也。" + }, + "3": { + "name": "九三", + "text": "贲如濡如,永贞吉。", + "image": "永贞之吉,终莫之陵也。" + }, + "4": { + "name": "六四", + "text": "贲如皤如,白马翰如,匪寇婚媾。", + "image": "当位疑也。匪寇婚媾,终无尤也。" + }, + "5": { + "name": "六五", + "text": "贲于丘园,束帛戋戋,吝,终吉。", + "image": "六五之吉,有喜也。" + }, + "6": { + "name": "上九", + "text": "白贲,无咎。", + "image": "白贲无咎,上得志也。" + } + } + }, + "(0, 0, 0, 0, 0, 1)": { + "name": "剥", + "text": "不利有攸往。", + "image": "山附地上,剥;上以厚下,安宅。", + "lines": { + "1": { + "name": "初六", + "text": "剥床以足,蔑贞凶。", + "image": "剥床以足,以灭下也。" + }, + "2": { + "name": "六二", + "text": "剥床以辨,蔑贞凶。", + "image": "剥床以辨,未有与也。" + }, + "3": { + "name": "六三", + "text": "剥之,无咎。", + "image": "剥之无咎,失上下也。" + }, + "4": { + "name": "六四", + "text": "剥床以肤,凶。", + "image": "剥床以肤,切近灾也。" + }, + "5": { + "name": "六五", + "text": "贯鱼,以宫人宠,无不利。", + "image": "以宫人宠,终无尤也。" + }, + "6": { + "name": "上九", + "text": "硕果不食,君子得舆,小人剥庐。", + "image": "君子得舆,民所载也。小人剥庐,终不可用也。" + } + } + }, + "(1, 0, 0, 0, 0, 0)": { + "name": "复", + "text": "亨。出入无疾,朋来无咎。反复其道,七日来复,利有攸往。", + "image": "雷在地中,复;先王以至日闭关,商旅不行,后不省方。", + "lines": { + "1": { + "name": "初九", + "text": "不远复,无祇悔,元吉。", + "image": "不远之复,以修身也。" + }, + "2": { + "name": "六二", + "text": "休复,吉。", + "image": "休复之吉,以下仁也。" + }, + "3": { + "name": "六三", + "text": "频复,厉无咎。", + "image": "频复之厉,义无咎也。" + }, + "4": { + "name": "六四", + "text": "中行独复。", + "image": "中行独复,以从道也。" + }, + "5": { + "name": "六五", + "text": "敦复,无悔。", + "image": "敦复无悔,中以自考也。" + }, + "6": { + "name": "上六", + "text": "迷复,凶,有灾眚。用行师,终有大败,以其国君,凶;至于十年,不克征。", + "image": "迷复之凶,反君道也。" + } + } + }, + "(1, 0, 0, 1, 1, 1)": { + "name": "无妄", + "text": "元亨,利贞。其匪正有眚,不利有攸往。", + "image": "天下雷行,物与无妄;先王以茂对时,育万物。", + "lines": { + "1": { + "name": "初九", + "text": "无妄,往吉。", + "image": "无妄之往,得志也。" + }, + "2": { + "name": "六二", + "text": "不耕获,不灾畲,则利有攸往。", + "image": "不耕获,未富也。" + }, + "3": { + "name": "六三", + "text": "无妄之灾,或系之牛,行人之得,邑人之灾。", + "image": "行人得牛,邑人灾也。" + }, + "4": { + "name": "九四", + "text": "可贞,无咎。", + "image": "可贞无咎,固有之也。" + }, + "5": { + "name": "九五", + "text": "无妄之疾,勿药有喜。", + "image": "无妄之药,不可试也。" + }, + "6": { + "name": "上九", + "text": "无妄,行有眚,无攸利。", + "image": "无妄之行,穷之灾也。" + } + } + }, + "(1, 1, 1, 0, 0, 1)": { + "name": "大畜", + "text": "利贞,不家食吉,利涉大川。", + "image": "天在山中,大畜;君子以多识前言往行,以畜其德。", + "lines": { + "1": { + "name": "初九", + "text": "有厉利已。", + "image": "有厉利已,不犯灾也。" + }, + "2": { + "name": "九二", + "text": "舆说輹。", + "image": "舆说輹,中无尤也。" + }, + "3": { + "name": "九三", + "text": "良马逐,利艰贞,曰闲舆卫,利有攸往。", + "image": "利有攸往,上合志也。" + }, + "4": { + "name": "六四", + "text": "童牛之牿,元吉。", + "image": "六四元吉,有喜也。" + }, + "5": { + "name": "六五", + "text": "豮豕之牙,吉。", + "image": "六五之吉,有庆也。" + }, + "6": { + "name": "上九", + "text": "何天之衢,亨。", + "image": "何天之衢,道大行也。" + } + } + }, + "(1, 0, 0, 0, 0, 1)": { + "name": "颐", + "text": "贞吉。观颐,自求口实。", + "image": "山下有雷,颐;君子以慎言语,节饮食。", + "lines": { + "1": { + "name": "初九", + "text": "舍尔灵龟,观我朵颐,凶。", + "image": "观我朵颐,亦不足贵也。" + }, + "2": { + "name": "六二", + "text": "颠颐,拂经,于丘颐,征凶。", + "image": "六二征凶,行失类也。" + }, + "3": { + "name": "六三", + "text": "拂颐,贞凶,十年勿用,无攸利。", + "image": "十年勿用,道大悖也。" + }, + "4": { + "name": "六四", + "text": "颠颐,吉;虎视眈眈,其欲逐逐,无咎。", + "image": "颠颐之吉,上施光也。" + }, + "5": { + "name": "六五", + "text": "拂经,居贞吉,不可涉大川。", + "image": "居贞之吉,顺以从上也。" + }, + "6": { + "name": "上九", + "text": "由颐,厉吉,利涉大川。", + "image": "由颐厉吉,大有庆也。" + } + } + }, + "(0, 1, 1, 1, 1, 0)": { + "name": "大过", + "text": "栋挠,利有攸往,亨。", + "image": "泽灭木,大过;君子以独立不惧,遁世无闷。", + "lines": { + "1": { + "name": "初六", + "text": "藉用白茅,无咎。", + "image": "藉用白茅,柔在下也。" + }, + "2": { + "name": "九二", + "text": "枯杨生稊,老夫得其女妻,无不利。", + "image": "老夫女妻,过以相与也。" + }, + "3": { + "name": "九三", + "text": "栋桡,凶。", + "image": "栋桡之凶,不可以有辅也。" + }, + "4": { + "name": "九四", + "text": "栋隆,吉;有它吝。", + "image": "栋隆之吉,不桡乎下也。" + }, + "5": { + "name": "九五", + "text": "枯杨生华,老妇得士夫,无咎无誉。", + "image": "枯杨生华,何可久也。老妇士夫,亦可丑也。" + }, + "6": { + "name": "上六", + "text": "过涉灭顶,凶,无咎。", + "image": "过涉之凶,不可咎也。" + } + } + }, + "(0, 1, 0, 0, 1, 0)": { + "name": "坎", + "text": "有孚,维心亨,行有尚。", + "image": "水洊至,习坎;君子以常德行,习教事。", + "lines": { + "1": { + "name": "初六", + "text": "习坎,入于坎窞,凶。", + "image": "习坎入坎,失道凶也。" + }, + "2": { + "name": "九二", + "text": "坎有险,求小得。", + "image": "求小得,未出中也。" + }, + "3": { + "name": "六三", + "text": "来之坎坎,险且枕,入于坎窞,勿用。", + "image": "来之坎坎,终无功也。" + }, + "4": { + "name": "六四", + "text": "樽酒簋贰,用缶,纳约自牖,终无咎。", + "image": "樽酒簋贰,刚柔际也。" + }, + "5": { + "name": "九五", + "text": "坎不盈,只既平,无咎。", + "image": "坎不盈,中未大也。" + }, + "6": { + "name": "上六", + "text": "系用徽纆,置于丛棘,三岁不得,凶。", + "image": "上六失道,凶三岁也。" + } + } + }, + "(1, 0, 1, 1, 0, 1)": { + "name": "离", + "text": "利贞,亨。畜牝牛,吉。", + "image": "明两作离,大人以继明照于四方。", + "lines": { + "1": { + "name": "初九", + "text": "履错然,敬之无咎。", + "image": "履错之敬,以辟咎也。" + }, + "2": { + "name": "六二", + "text": "黄离,元吉。", + "image": "黄离元吉,得中道也。" + }, + "3": { + "name": "九三", + "text": "日昃之离,不鼓缶而歌,则大耋之嗟,凶。", + "image": "日昃之离,何可久也。" + }, + "4": { + "name": "九四", + "text": "突如其来如,焚如,死如,弃如。", + "image": "突如其来如,无所容也。" + }, + "5": { + "name": "六五", + "text": "出涕沱若,戚嗟若,吉。", + "image": "六五之吉,离王公也。" + }, + "6": { + "name": "上九", + "text": "王用出征,有嘉折首,获匪其丑,无咎。", + "image": "王用出征,以正邦也。" + } + } + }, + "(0, 0, 1, 1, 1, 0)": { + "name": "咸", + "text": "亨,利贞,取女吉。", + "image": "山上有泽,咸;君子以虚受人。", + "lines": { + "1": { + "name": "初六", + "text": "咸其拇。", + "image": "咸其拇,志在外也。" + }, + "2": { + "name": "六二", + "text": "咸其腓,凶,居吉。", + "image": "虽凶居吉,顺不害也。" + }, + "3": { + "name": "九三", + "text": "咸其股,执其随,往吝。", + "image": "咸其股,亦不处也。志在随人,所执下也。" + }, + "4": { + "name": "九四", + "text": "贞吉,悔亡,憧憧往来,朋从尔思。", + "image": "贞吉悔亡,未感害也。憧憧往来,未光大也。" + }, + "5": { + "name": "九五", + "text": "咸其脢,无悔。", + "image": "咸其脢,志末也。" + }, + "6": { + "name": "上六", + "text": "咸其辅颊舌。", + "image": "咸其辅颊舌,滕口说也。" + } + } + }, + "(0, 1, 1, 1, 0, 0)": { + "name": "恒", + "text": "亨,无咎,利贞,利有攸往。", + "image": "雷风恒;君子以立不易方。", + "lines": { + "1": { + "name": "初六", + "text": "浚恒,贞凶,无攸利。", + "image": "浚恒之凶,始求深也。" + }, + "2": { + "name": "九二", + "text": "悔亡。", + "image": "九二悔亡,能久中也。" + }, + "3": { + "name": "九三", + "text": "不恒其德,或承之羞,贞吝。", + "image": "不恒其德,无所容也。" + }, + "4": { + "name": "九四", + "text": "田无禽。", + "image": "久非其位,安得禽也。" + }, + "5": { + "name": "六五", + "text": "恒其德,贞;妇人吉,夫子凶。", + "image": "妇人贞吉,从一而终也。夫子制义,从妇凶也。" + }, + "6": { + "name": "上六", + "text": "振恒,凶。", + "image": "振恒在上,大无功也。" + } + } + }, + "(0, 0, 1, 1, 1, 1)": { + "name": "遁", + "text": "亨,小利贞。", + "image": "天下有山,遁;君子以远小人,不恶而严。", + "lines": { + "1": { + "name": "初六", + "text": "遁尾,厉,勿用有攸往。", + "image": "遁尾之厉,不往何灾也。" + }, + "2": { + "name": "六二", + "text": "执之用黄牛之革,莫之胜说。", + "image": "执用黄牛,固志也。" + }, + "3": { + "name": "九三", + "text": "系遁,有疾厉,畜臣妾吉。", + "image": "系遁之厉,有疾惫也。畜臣妾吉,不可大事也。" + }, + "4": { + "name": "九四", + "text": "好遁,君子吉,小人否。", + "image": "君子好遁,小人否也。" + }, + "5": { + "name": "九五", + "text": "嘉遁,贞吉。", + "image": "嘉遁贞吉,以正志也。" + }, + "6": { + "name": "上九", + "text": "肥遁,无不利。", + "image": "肥遁无不利,无所疑也。" + } + } + }, + "(1, 1, 1, 1, 0, 0)": { + "name": "大壮", + "text": "利贞。", + "image": "雷在天上,大壮;君子以非礼勿履。", + "lines": { + "1": { + "name": "初九", + "text": "壮于趾,征凶,有孚。", + "image": "壮于趾,其孚穷也。" + }, + "2": { + "name": "九二", + "text": "贞吉。", + "image": "九二贞吉,以中也。" + }, + "3": { + "name": "九三", + "text": "小人用壮,君子用罔,贞厉,羝羊触藩,羸其角。", + "image": "小人用壮,君子罔也。" + }, + "4": { + "name": "九四", + "text": "贞吉悔亡;藩决不羸,壮于大舆之輹。", + "image": "藩决不羸,尚往也。" + }, + "5": { + "name": "六五", + "text": "丧羊于易,无悔。", + "image": "丧羊于易,位不当也。" + }, + "6": { + "name": "上六", + "text": "羝羊触藩,不能退,不能遂,无攸利,艰则吉。", + "image": "不能退,不能遂,不祥也。艰则吉,咎不长也。" + } + } + }, + "(0, 0, 0, 1, 0, 1)": { + "name": "晋", + "text": "康侯用锡马蕃庶,昼日三接。", + "image": "明出地上,晋;君子以自昭明德。", + "lines": { + "1": { + "name": "初六", + "text": "晋如,摧如,贞吉。罔孚,裕无咎。", + "image": "晋如,摧如;独行正也。裕无咎;未受命也。" + }, + "2": { + "name": "六二", + "text": "晋如,愁如,贞吉;受兹介福,于其王母。", + "image": "受兹介福,以中正也。" + }, + "3": { + "name": "六三", + "text": "众允,悔亡。", + "image": "众允之,志上行也。" + }, + "4": { + "name": "九四", + "text": "晋如硕鼠,贞厉。", + "image": "硕鼠贞厉,位不当也。" + }, + "5": { + "name": "六五", + "text": "悔亡,失得勿恤,往吉,无不利。", + "image": "失得勿恤,往有庆也。" + }, + "6": { + "name": "上九", + "text": "晋其角,维用伐邑,厉吉无咎,贞吝。", + "image": "维用伐邑,道未光也。" + } + } + }, + "(1, 0, 1, 0, 0, 0)": { + "name": "明夷", + "text": "利艰贞。", + "image": "明入地中,明夷;君子以莅众,用晦而明。", + "lines": { + "1": { + "name": "初九", + "text": "明夷于飞,垂其翼;君子于行,三日不食,有攸往,主人有言。", + "image": "君子于行,义不食也。" + }, + "2": { + "name": "六二", + "text": "明夷,夷于左股,用拯马壮,吉。", + "image": "六二之吉,顺以则也。" + }, + "3": { + "name": "九三", + "text": "明夷于南狩,得其大首,不可疾贞。", + "image": "南狩之志,乃大得也。" + }, + "4": { + "name": "六四", + "text": "入于左腹,获明夷之心,出于门庭。", + "image": "入于左腹,获心意也。" + }, + "5": { + "name": "六五", + "text": "箕子之明夷,利贞。", + "image": "箕子之贞,明不可息也。" + }, + "6": { + "name": "上六", + "text": "不明晦,初登于天,后入于地。", + "image": "初登于天,照四国也。后入于地,失则也。" + } + } + }, + "(1, 0, 1, 0, 1, 1)": { + "name": "家人", + "text": "利女贞。", + "image": "风自火出,家人;君子以言有物,而行有恒。", + "lines": { + "1": { + "name": "初九", + "text": "闲有家,悔亡。", + "image": "闲有家,志未变也。" + }, + "2": { + "name": "六二", + "text": "无攸遂,在中馈,贞吉。", + "image": "六二之吉,顺以巽也。" + }, + "3": { + "name": "九三", + "text": "九三:家人嗃嗃,悔厉吉;妇子嘻嘻,终吝。", + "image": "家人嗃嗃,未失也;妇子嘻嘻,失家节也。" + }, + "4": { + "name": "九四", + "text": "富家,大吉。", + "image": "富家大吉,顺在位也。" + }, + "5": { + "name": "九五", + "text": "王假有家,勿恤,往吉。", + "image": "王假有家,交相爱也。" + }, + "6": { + "name": "上九", + "text": "有孚威如,终吉。", + "image": "威如之吉,反身之谓也。" + } + } + }, + "(1, 1, 0, 1, 0, 1)": { + "name": "睽", + "text": "小事吉。", + "image": "上火下泽,睽;君子以同而异。", + "lines": { + "1": { + "name": "初九", + "text": "悔亡,丧马勿逐,自复;见恶人无咎。", + "image": "见恶人,以辟咎也。" + }, + "2": { + "name": "九二", + "text": "遇主于巷,无咎。", + "image": "遇主于巷,未失道也。" + }, + "3": { + "name": "六三", + "text": "见舆曳,其牛掣,其人天且劓,无初有终。", + "image": "见舆曳,位不当也。无初有终,遇刚也。" + }, + "4": { + "name": "九四", + "text": "睽孤,遇元夫,交孚,厉无咎。", + "image": "交孚无咎,志行也。" + }, + "5": { + "name": "六五", + "text": "悔亡,厥宗噬肤,往何咎。", + "image": "厥宗噬肤,往有庆也。" + }, + "6": { + "name": "上九", + "text": "睽孤,见豕负涂,载鬼一车,先张之弧,后说之弧;匪寇婚媾;往遇雨则吉。", + "image": "遇雨之吉,群疑亡也。" + } + } + }, + "(0, 0, 1, 0, 1, 0)": { + "name": "蹇", + "text": "利西南,不利东北;利见大人,贞吉。", + "image": "山上有水,蹇;君子以反身修德。", + "lines": { + "1": { + "name": "初六", + "text": "往蹇,来誉。", + "image": "往蹇来誉,宜待也。" + }, + "2": { + "name": "六二", + "text": "王臣蹇蹇,匪躬之故。", + "image": "王臣蹇蹇,终无尤也。" + }, + "3": { + "name": "九三", + "text": "往蹇来反。", + "image": "往蹇来反,内喜之也。" + }, + "4": { + "name": "六四", + "text": "往蹇来连。", + "image": "往蹇来连,当位实也。" + }, + "5": { + "name": "九五", + "text": "大蹇朋来。", + "image": "大蹇朋来,以中节也。" + }, + "6": { + "name": "上六", + "text": "往蹇来硕,吉;利见大人。", + "image": "往蹇来硕,志在内也。利见大人,以从贵也。" + } + } + }, + "(0, 1, 0, 1, 0, 0)": { + "name": "解", + "text": "利西南,无所往,其来复吉;有攸往,夙吉。", + "image": "雷雨作,解;君子以赦过宥罪。", + "lines": { + "1": { + "name": "初六", + "text": "无咎。", + "image": "刚柔之际,义无咎也。" + }, + "2": { + "name": "九二", + "text": "田获三狐,得黄矢,贞吉。", + "image": "九二贞吉,得中道也。" + }, + "3": { + "name": "六三", + "text": "负且乘,致寇至,贞吝。", + "image": "负且乘,亦可丑也。自我致戎,又谁咎也。" + }, + "4": { + "name": "九四", + "text": "解而拇,朋至斯孚。", + "image": "解而拇,未当位也。" + }, + "5": { + "name": "六五", + "text": "君子维有解,吉;有孚于小人。", + "image": "君子有解,小人退也。" + }, + "6": { + "name": "上六", + "text": "公用射隼于高墉之上,获之,无不利。", + "image": "公用射隼,以解悖也。" + } + } + }, + "(1, 1, 0, 0, 0, 1)": { + "name": "损", + "text": "有孚,元吉,无咎,可贞,利有攸往。曷之用,二簋可用享。", + "image": "山下有泽,损;君子以惩忿窒欲。", + "lines": { + "1": { + "name": "初九", + "text": "己事遄往,无咎,酌损之。", + "image": "己事遄往,尚合志也。" + }, + "2": { + "name": "九二", + "text": "利贞,征凶,弗损,益之。", + "image": "九二利贞,中以为志也。" + }, + "3": { + "name": "六三", + "text": "三人行,则损一人;一人行,则得其友。", + "image": "一人行,三则疑也。" + }, + "4": { + "name": "六四", + "text": "损其疾,使遄有喜,无咎。", + "image": "损其疾,亦可喜也。" + }, + "5": { + "name": "六五", + "text": "或益之十朋之龟,弗克违,元吉。", + "image": "六五元吉,自上佑也。" + }, + "6": { + "name": "上九", + "text": "弗损益之,无咎,贞吉,利有攸往,得臣无家。", + "image": "弗损益之,大得志也。" + } + } + }, + "(1, 0, 0, 0, 1, 1)": { + "name": "益", + "text": "利有攸往,利涉大川。", + "image": "风雷,益;君子以见善则迁,有过则改。", + "lines": { + "1": { + "name": "初九", + "text": "利用为大作,元吉,无咎。", + "image": "元吉无咎,下不厚事也。" + }, + "2": { + "name": "六二", + "text": "或益之十朋之龟,弗克违,永贞吉;王用享于帝,吉。", + "image": "或益之,自外来也。" + }, + "3": { + "name": "六三", + "text": "益之用凶事,无咎。有孚中行,告公用圭。", + "image": "益用凶事,固有之也。" + }, + "4": { + "name": "六四", + "text": "中行告公从,利用为依迁国。", + "image": "告公从,以益志也。" + }, + "5": { + "name": "九五", + "text": "有孚惠心,勿问元吉。有孚惠我德。", + "image": "有孚惠心,勿问之矣。惠我德,大得志也。" + }, + "6": { + "name": "上九", + "text": "莫益之,或击之,立心勿恒,凶。", + "image": "莫益之,偏辞也。或击之,自外来也。" + } + } + }, + "(1, 1, 1, 1, 1, 0)": { + "name": "夬", + "text": "扬于王庭,孚号,有厉,告自邑,不利即戎,利有攸往。", + "image": "泽上于天,夬;君子以施禄及下,居德则忌。", + "lines": { + "1": { + "name": "初九", + "text": "壮于前趾,往不胜为咎。", + "image": "往不胜为咎,得志也。" + }, + "2": { + "name": "九二", + "text": "惕号,莫夜有戎,勿恤。", + "image": "有戎勿恤,得中道也。" + }, + "3": { + "name": "九三", + "text": "壮于頄,有凶。君子夬夬,独行遇雨,若濡有愠,无咎。", + "image": "君子夬夬,终无咎也。" + }, + "4": { + "name": "九四", + "text": "臀无肤,其行次且。牵羊悔亡,闻言不信。", + "image": "其行次且,位不当也。闻言不信,聪不明也。" + }, + "5": { + "name": "九五", + "text": "苋陆夬夬,中行无咎。", + "image": "中行无咎,中未光也。" + }, + "6": { + "name": "上六", + "text": "无号,终有凶。", + "image": "无号之凶,终不可长也。" + } + } + }, + "(0, 1, 1, 1, 1, 1)": { + "name": "姤", + "text": "女壮,勿用取女。", + "image": "天下有风,姤;后以施命诰四方。", + "lines": { + "1": { + "name": "初六", + "text": "系于金柅,贞吉,有攸往,见凶,羸豕孚蹢躅。", + "image": "系于金柅,柔道牵也。" + }, + "2": { + "name": "九二", + "text": "包有鱼,无咎,不利宾。", + "image": "包有鱼,义不及宾也。" + }, + "3": { + "name": "九三", + "text": "臀无肤,其行次且,厉,无大咎。", + "image": "其行次且,行未牵也。" + }, + "4": { + "name": "九四", + "text": "包无鱼,起凶。", + "image": "无鱼之凶,远民也。" + }, + "5": { + "name": "九五", + "text": "以杞包瓜,含章,有陨自天。", + "image": "九五含章,中正也。有陨自天,志不舍命也。" + }, + "6": { + "name": "上九", + "text": "姤其角,吝,无咎。", + "image": "姤其角,上穷吝也。" + } + } + }, + "(0, 0, 0, 1, 1, 0)": { + "name": "萃", + "text": "亨,王假有庙,利见大人,亨,利贞,用大牲吉,利有攸往。", + "image": "泽上于地,萃;君子以除戎器,戒不虞。", + "lines": { + "1": { + "name": "初六", + "text": "有孚不终,乃乱乃萃,若号,一握为笑,勿恤,往无咎。", + "image": "乃乱乃萃,其志乱也。" + }, + "2": { + "name": "六二", + "text": "引吉,无咎,孚乃利用禴。", + "image": "引吉无咎,中未变也。" + }, + "3": { + "name": "六三", + "text": "萃如,嗟如,无攸利,往无咎,小吝。", + "image": "往无咎,上巽也。" + }, + "4": { + "name": "九四", + "text": "大吉,无咎。", + "image": "大吉无咎,位不当也。" + }, + "5": { + "name": "九五", + "text": "萃有位,无咎;匪孚,元永贞,悔亡。", + "image": "萃有位,志未光也。" + }, + "6": { + "name": "上六", + "text": "赍咨涕洟,无咎。", + "image": "赍咨涕洟,未安上也。" + } + } + }, + "(0, 1, 1, 0, 0, 0)": { + "name": "升", + "text": "元亨,用见大人,勿恤,南征吉。", + "image": "地中生木,升;君子以顺德,积小以高大。", + "lines": { + "1": { + "name": "初六", + "text": "允升,大吉。", + "image": "允升大吉,上合志也。" + }, + "2": { + "name": "九二", + "text": "孚乃利用禴,无咎。", + "image": "九二之孚,有喜也。" + }, + "3": { + "name": "九三", + "text": "升虚邑。", + "image": "升虚邑,无所疑也。" + }, + "4": { + "name": "六四", + "text": "王用亨于岐山,吉,无咎。", + "image": "王用亨于岐山,顺事也。" + }, + "5": { + "name": "六五", + "text": "贞吉,升阶。", + "image": "贞吉升阶,大得志也。" + }, + "6": { + "name": "上六", + "text": "冥升,利于不息之贞。", + "image": "冥升在上,消不富也。" + } + } + }, + "(0, 1, 0, 1, 1, 0)": { + "name": "困", + "text": "亨,贞,大人吉,无咎,有言不信。", + "image": "泽无水,困;君子以致命遂志。", + "lines": { + "1": { + "name": "初六", + "text": "臀困于株木,入于幽谷,三岁不见。", + "image": "入于幽谷,幽不明也。" + }, + "2": { + "name": "九二", + "text": "困于酒食,朱绂方来,利用享祀,征凶,无咎。", + "image": "困于酒食,中有庆也。" + }, + "3": { + "name": "六三", + "text": "困于石,据于蒺藜,入于其宫,不见其妻,凶。", + "image": "据于蒺蔾,乘刚也。入于其宫,不见其妻,不祥也。" + }, + "4": { + "name": "九四", + "text": "来徐徐,困于金车,吝,有终。", + "image": "来徐徐,志在下也。虽不当位,有与也。" + }, + "5": { + "name": "九五", + "text": "劓刖,困于赤绂,乃徐有说,利用祭祀。", + "image": "劓刖,志未得也。乃徐有说,以中直也。利用祭祀,受福也。" + }, + "6": { + "name": "上六", + "text": "困于葛藟,于臲卼,曰动悔,有悔,征吉。", + "image": "困于葛藟,未当也。动悔,有悔吉,行也。" + } + } + }, + "(0, 1, 1, 0, 1, 0)": { + "name": "井", + "text": "改邑不改井,无丧无得,往来井井,汔至,,亦未繘井,羸其瓶,凶。", + "image": "木上有水,井;君子以劳民劝相。", + "lines": { + "1": { + "name": "初六", + "text": "井泥不食,旧井无禽。", + "image": "井泥不食,下也。旧井无禽,时舍也。" + }, + "2": { + "name": "九二", + "text": "井谷射鲋,瓮敝漏。", + "image": "井谷射鲋,无与也。" + }, + "3": { + "name": "九三", + "text": "井渫不食,为我心恻,可用汲,王明,并受其福。", + "image": "井渫不食,行恻也。求王明,受福也。" + }, + "4": { + "name": "六四", + "text": "井甃,无咎。", + "image": "井甃无咎,修井也。" + }, + "5": { + "name": "九五", + "text": "井洌,寒泉食。", + "image": "寒泉之食,中正也。" + }, + "6": { + "name": "上六", + "text": "井收勿幕,有孚元吉。", + "image": "元吉在上,大成也。" + } + } + }, + "(1, 0, 1, 1, 1, 0)": { + "name": "革", + "text": "己日乃孚,元亨,利贞,悔亡。", + "image": "泽中有火,革;君子以治历明时。", + "lines": { + "1": { + "name": "初九", + "text": "巩用黄牛之革。", + "image": "巩用黄牛,不可以有为也。" + }, + "2": { + "name": "六二", + "text": "己日乃革之,征吉,无咎。", + "image": "己日革之,行有嘉也。" + }, + "3": { + "name": "九三", + "text": "征凶,贞厉,革言三就,有孚。", + "image": "革言三就,又何之矣。" + }, + "4": { + "name": "九四", + "text": "悔亡,有孚改命,吉。", + "image": "改命之吉,信志也。" + }, + "5": { + "name": "九五", + "text": "大人虎变,未占有孚。", + "image": "大人虎变,其文炳也。" + }, + "6": { + "name": "上九", + "text": "君子豹变,小人革面,征凶,居贞吉。", + "image": "君子豹变,其文蔚也。小人革面,顺以从君也。" + } + } + }, + "(0, 1, 1, 1, 0, 1)": { + "name": "鼎", + "text": "元吉,亨。", + "image": "木上有火,鼎;君子以正位凝命。", + "lines": { + "1": { + "name": "初六", + "text": "鼎颠趾,利出否,得妾以其子,无咎。", + "image": "鼎颠趾,未悖也。利出否,以从贵也。" + }, + "2": { + "name": "九二", + "text": "鼎有实,我仇有疾,不我能即,吉。", + "image": "鼎有实,慎所之也。我仇有疾,终无尤也。" + }, + "3": { + "name": "九三", + "text": "鼎耳革,其行塞,雉膏不食,方雨亏悔,终吉。", + "image": "鼎耳革,失其义也。" + }, + "4": { + "name": "九四", + "text": "鼎折足,覆公餗,其形渥,凶。", + "image": "覆公餗,信如何也。" + }, + "5": { + "name": "六五", + "text": "鼎黄耳金铉,利贞。", + "image": "鼎黄耳,中以为实也。" + }, + "6": { + "name": "上九", + "text": "鼎玉铉,大吉,无不利。", + "image": "玉铉在上,刚柔节也。" + } + } + }, + "(1, 0, 0, 1, 0, 0)": { + "name": "震", + "text": "亨。震来虩虩,笑言哑哑。震惊百里,不丧匕鬯。", + "image": "洊雷,震;君子以恐惧修省。", + "lines": { + "1": { + "name": "初九", + "text": "震来虩虩,后笑言哑哑,吉。", + "image": "震来虩虩,恐致福也。笑言哑哑,后有则也。" + }, + "2": { + "name": "六二", + "text": "震来厉,亿丧贝,跻于九陵,勿逐,七日得。", + "image": "震来厉,乘刚也。" + }, + "3": { + "name": "六三", + "text": "震苏苏,震行无眚。", + "image": "震苏苏,位不当也。" + }, + "4": { + "name": "九四", + "text": "震遂泥。", + "image": "震遂泥,未光也。" + }, + "5": { + "name": "六五", + "text": "震往来厉,亿无丧,有事。", + "image": "震往来厉,危行也。其事在中,大无丧也。" + }, + "6": { + "name": "上六", + "text": "震索索,视矍矍,征凶,震不于其躬,于其邻,无咎,婚媾有言。", + "image": "震索索,中未得也。虽凶无咎,畏邻戒也。" + } + } + }, + "(0, 0, 1, 0, 0, 1)": { + "name": "艮", + "text": "艮其背,不获其身,行其庭,不见其人,无咎。", + "image": "兼山,艮;君子以思不出其位。", + "lines": { + "1": { + "name": "初六", + "text": "艮其趾,无咎,利永贞。", + "image": "艮其趾,未失正也。" + }, + "2": { + "name": "六二", + "text": "艮其腓,不拯其随,其心不快。", + "image": "不拯其随,未退听也。" + }, + "3": { + "name": "九三", + "text": "艮其限,列其夤,厉熏心。", + "image": "艮其限,危熏心也。" + }, + "4": { + "name": "六四", + "text": "艮其身,无咎。", + "image": "艮其身,止诸躬也。" + }, + "5": { + "name": "六五", + "text": "艮其辅,言有序,悔亡。", + "image": "艮其辅,以中正也。" + }, + "6": { + "name": "上九", + "text": "敦艮,吉。", + "image": "敦艮之吉,以厚终也。" + } + } + }, + "(0, 0, 1, 0, 1, 1)": { + "name": "渐", + "text": "女归吉,利贞。", + "image": "山上有木,渐;君子以居贤德,善俗。", + "lines": { + "1": { + "name": "初六", + "text": "鸿渐于干,小子厉,有言,无咎。", + "image": "小子之厉,义无咎也。" + }, + "2": { + "name": "六二", + "text": "鸿渐于磐,饮食衎衎,吉。", + "image": "饮食衎衎,吉,不素饱也。" + }, + "3": { + "name": "九三", + "text": "鸿渐于陆,夫征不复,妇孕不育,凶;利御寇。", + "image": "夫征不复,离群丑也。妇孕不育,失其道也。利用御寇,顺相保也。" + }, + "4": { + "name": "六四", + "text": "鸿渐于木,或得其桷,无咎。", + "image": "或得其桷,顺以巽也。" + }, + "5": { + "name": "九五", + "text": "鸿渐于陵,妇三岁不孕,终莫之胜,吉。", + "image": "终莫之胜,吉;得所愿也。" + }, + "6": { + "name": "上九", + "text": "鸿渐于陆,其羽可用为仪,吉。", + "image": "其羽可用为仪,吉;不可乱也。" + } + } + }, + "(1, 1, 0, 1, 0, 0)": { + "name": "归妹", + "text": "征凶,无攸利。", + "image": "泽上有雷,归妹;君子以永终知敝。", + "tuan": "归妹,天地之大义也。天地不交,而万物不兴,归妹人之终始也。说以动,所归妹也。征凶,位不当也。无攸利,柔乘刚也。", + "lines": { + "1": { + "name": "初九", + "text": "归妹以娣,跛能履,征吉。", + "image": "归妹以娣,以恒也。跛能履吉,相承也。" + }, + "2": { + "name": "九二", + "text": "眇能视,利幽人之贞。", + "image": "利幽人之贞,未变常也。" + }, + "3": { + "name": "六三", + "text": "归妹以须,反归以娣。", + "image": "归妹以须,未当也。" + }, + "4": { + "name": "九四", + "text": "归妹愆期,迟归有时。", + "image": "愆期之志,有待而行也。" + }, + "5": { + "name": "六五", + "text": "帝乙归妹,其君之袂,不如其娣之袂良;月几望,吉。", + "image": "帝乙归妹,不如其娣之袂良也。其位在中,以贵行也。" + }, + "6": { + "name": "上六", + "text": "女承筐无实,士刲羊无血,无攸利。", + "image": "上六无实,承虚筐也。" + } + } + }, + "(1, 0, 1, 1, 0, 0)": { + "name": "丰", + "text": "亨,王假之,勿忧,宜日中。", + "image": "雷电皆至,丰;君子以折狱致刑。", + "tuan": "丰,大也。明以动,故丰。王假之,尚大也。勿忧宜日中,宜照天下也。日中则昃,月盈则食,天地盈虚,与时消息,而况人于人乎?况于鬼神乎?", + "lines": { + "1": { + "name": "初九", + "text": "遇其配主,虽旬无咎,往有尚。", + "image": "虽旬无咎,过旬灾也。" + }, + "2": { + "name": "六二", + "text": "丰其蔀,日中见斗,往得疑疾,有孚发若,吉。", + "image": "有孚发若,信以发志也。" + }, + "3": { + "name": "九三", + "text": "丰其沛,日中见沬,折其右肱,无咎。", + "image": "丰其沛,不可大事也。折其右肱,终不可用也。" + }, + "4": { + "name": "九四", + "text": "丰其蔀,日中见斗,遇其夷主,吉。", + "image": "丰其蔀,位未当也。日中见斗,幽不明也。遇其夷主,吉;行也。" + }, + "5": { + "name": "六五", + "text": "来章,有庆誉,吉。", + "image": "六五之吉,有庆也。" + }, + "6": { + "name": "上六", + "text": "丰其屋,蔀其家,闚其户,阒其无人,三岁不觌,凶。", + "image": "丰其屋,天际翔也。闚其户,阒其无人,自藏也。" + } + } + }, + "(0, 0, 1, 1, 0, 1)": { + "name": "旅", + "text": "小亨,旅贞吉。", + "image": "山上有火,旅;君子以明慎用刑,而不留狱。", + "tuan": "旅,小亨,柔得中乎外,而顺乎刚,止而丽乎明,是以小亨,旅贞吉也。旅之时义大矣哉!", + "lines": { + "1": { + "name": "初六", + "text": "旅琐琐,斯其所取灾。", + "image": "旅琐琐,志穷灾也。" + }, + "2": { + "name": "六二", + "text": "旅即次,怀其资,得童仆贞。", + "image": "得童仆贞,终无尤也。" + }, + "3": { + "name": "九三", + "text": "旅焚其次,丧其童仆,贞厉。", + "image": "旅焚其次,亦以伤矣。以旅与下,其义丧也。" + }, + "4": { + "name": "九四", + "text": "旅于处,得其资斧,我心不快。", + "image": "旅于处,未得位也。得其资斧,心未快也。" + }, + "5": { + "name": "六五", + "text": "射雉一矢亡,终以誉命。", + "image": "终以誉命,上逮也。" + }, + "6": { + "name": "上九", + "text": "鸟焚其巢,旅人先笑后号啕。丧牛于易,凶。", + "image": "以旅在上,其义焚也。丧牛于易,终莫之闻也。" + } + } + }, + "(0, 1, 1, 0, 1, 1)": { + "name": "巽", + "text": "小亨,利有攸往,利见大人。", + "image": "随风,巽;君子以申命行事。", + "tuan": "重巽以申命,刚巽乎中正而志行。柔皆顺乎刚,是以小亨,利有攸往,利见大人。", + "lines": { + "1": { + "name": "初六", + "text": "进退,利武人之贞。", + "image": "进退,志疑也。利武人之贞,志治也。" + }, + "2": { + "name": "九二", + "text": "巽在床下,用史巫纷若,吉,无咎。", + "image": "纷若之吉,得中也。" + }, + "3": { + "name": "九三", + "text": "频巽,吝。", + "image": "频巽之吝,志穷也。" + }, + "4": { + "name": "六四", + "text": "悔亡,田获三品。", + "image": "田获三品,有功也。" + }, + "5": { + "name": "九五", + "text": "贞吉悔亡,无不利,无初有终,先庚三日,后庚三日,吉。", + "image": "九五之吉,位正中也。" + }, + "6": { + "name": "上九", + "text": "巽在床下,丧其资斧,贞凶。", + "image": "巽在床下,上穷也。丧其资斧,正乎凶也。" + } + } + }, + "(1, 1, 0, 1, 1, 0)": { + "name": "兑", + "text": "亨,利贞。", + "image": "丽泽,兑;君子以朋友讲习。", + "tuan": "兑,说也。刚中而柔外,说以利贞,是以顺乎天,而应乎人。说以先民,民忘其劳;说以犯难,民忘其死;说之大,民劝矣哉!", + "lines": { + "1": { + "name": "初九", + "text": "和兑,吉。", + "image": "和兑之吉,行未疑也。" + }, + "2": { + "name": "九二", + "text": "孚兑,吉,悔亡。", + "image": "孚兑之吉,信志也。" + }, + "3": { + "name": "六三", + "text": "来兑,凶。", + "image": "来兑之凶,位不当也。" + }, + "4": { + "name": "九四", + "text": "商兑,未宁,介疾有喜。", + "image": "九四之喜,有庆也。" + }, + "5": { + "name": "九五", + "text": "孚于剥,有厉。", + "image": "孚于剥,位正当也。" + }, + "6": { + "name": "上六", + "text": "引兑。", + "image": "上六引兑,未光也。" + } + } + }, + "(0, 1, 0, 0, 1, 1)": { + "name": "涣", + "text": "亨,王假有庙,利涉大川,利贞。", + "image": "风行水上,涣;先王以享于帝立庙。", + "tuan": "涣,亨。刚来而不穷,柔得位乎外而上同。王假有庙,王乃在中也。利涉大川,乘木有功也。", + "lines": { + "1": { + "name": "初六", + "text": "用拯马壮,吉。", + "image": "初六之吉,顺也。" + }, + "2": { + "name": "九二", + "text": "涣奔其机,悔亡。", + "image": "涣奔其机,得愿也。" + }, + "3": { + "name": "六三", + "text": "涣其躬,无悔。", + "image": "涣其躬,志在外也。" + }, + "4": { + "name": "六四", + "text": "涣其群,元吉。涣有丘,匪夷所思。", + "image": "涣其群,元吉;光大也。" + }, + "5": { + "name": "九五", + "text": "涣汗其大号,涣王居,无咎。", + "image": "王居无咎,正位也。" + }, + "6": { + "name": "上九", + "text": "涣其血,去逖出,无咎。", + "image": "涣其血,远害也。" + } + } + }, + "(1, 1, 0, 0, 1, 0)": { + "name": "节", + "text": "亨,苦节,不可贞。", + "image": "泽上有水,节;君子以制数度,议德行。", + "tuan": "节,亨,刚柔分,而刚得中。苦节不可贞,其道穷也。说以行险,当位以节,中正以通。天地节而四时成,节以制度,不伤财,不害民。", + "lines": { + "1": { + "name": "初九", + "text": "不出户庭,无咎。", + "image": "不出户庭,知通塞也。" + }, + "2": { + "name": "九二", + "text": "不出门庭,凶。", + "image": "不出门庭,失时极也。" + }, + "3": { + "name": "六三", + "text": "不节若,则嗟若,无咎。", + "image": "不节之嗟,又谁咎也。" + }, + "4": { + "name": "六四", + "text": "安节,亨。", + "image": "安节之亨,承上道也。" + }, + "5": { + "name": "九五", + "text": "甘节,吉;往有尚。", + "image": "甘节之吉,居位中也。" + }, + "6": { + "name": "上六", + "text": "苦节,贞凶,悔亡。", + "image": "苦节贞凶,其道穷也。" + } + } + }, + "(1, 1, 0, 0, 1, 1)": { + "name": "中孚", + "text": "豚鱼,吉,利涉大川,利贞。", + "image": "泽上有风,中孚;君子以议狱缓死。", + "tuan": "中孚,柔在内而刚得中。说而巽,孚,乃化邦也。豚鱼吉,信及豚鱼也。利涉大川,乘木舟虚也。中孚以利贞,乃应乎天也。", + "lines": { + "1": { + "name": "初九", + "text": "虞吉,有它不燕。", + "image": "初九虞吉,志未变也。" + }, + "2": { + "name": "九二", + "text": "鸣鹤在阴,其子和之,我有好爵,吾与尔靡之。", + "image": "其子和之,中心愿也。" + }, + "3": { + "name": "六三", + "text": "得敌,或鼓或罢,或泣或歌。", + "image": "可鼓或罢,位不当也。" + }, + "4": { + "name": "六四", + "text": "月几望,马匹亡,无咎。", + "image": "马匹亡,绝类上也。" + }, + "5": { + "name": "九五", + "text": "有孚挛如,无咎。", + "image": "有孚挛如,位正当也。" + }, + "6": { + "name": "上九", + "text": "翰音登于天,贞凶。", + "image": "翰音登于天,何可长也。" + } + } + }, + "(0, 0, 1, 1, 0, 0)": { + "name": "小过", + "text": "亨,利贞,可小事,不可大事。飞鸟遗之音,不宜上,宜下,大吉。", + "image": "山上有雷,小过;君子以行过乎恭,丧过乎哀,用过乎俭。", + "tuan": "小过,小者过而亨也。过以利贞,与时行也。柔得中,是以小事吉也。刚失位而不中,是以不可大事也。有飞鸟之象焉,有飞鸟遗之音,不宜上宜下,大吉;上逆而下顺也。", + "lines": { + "1": { + "name": "初六", + "text": "飞鸟以凶。", + "image": "飞鸟以凶,不可如何也。" + }, + "2": { + "name": "六二", + "text": "过其祖,遇其妣;不及其君,遇其臣,无咎。", + "image": "不及其君,臣不可过也。" + }, + "3": { + "name": "九三", + "text": "弗过防之,从或戕之,凶。", + "image": "从或戕之,凶如何也。" + }, + "4": { + "name": "九四", + "text": "无咎,弗过遇之,往厉必戒,勿用永贞。", + "image": "弗过遇之,位不当也。往厉必戒,终不可长也。" + }, + "5": { + "name": "六五", + "text": "密云不雨,自我西郊,公弋取彼在穴", + "image": "密云不雨,已上也。" + }, + "6": { + "name": "上六", + "text": "弗遇过之,飞鸟离之,凶,是谓灾眚。", + "image": "弗遇过之,已亢也。" + } + } + }, + "(1, 0, 1, 0, 1, 0)": { + "name": "既济", + "text": "亨小,利贞,初吉终乱。", + "image": "水在火上,既济;君子以思患而预防之。", + "tuan": "既济,亨,小者亨也。利贞,刚柔正而位当也。初吉,柔得中也。终止则乱,其道穷也。", + "lines": { + "1": { + "name": "初九", + "text": "曳其轮,濡其尾,无咎。", + "image": "曳其轮,义无咎也。" + }, + "2": { + "name": "六二", + "text": "妇丧其茀,勿逐,七日得。", + "image": "七日得,以中道也。" + }, + "3": { + "name": "九三", + "text": "高宗伐鬼方,三年克之,小人勿用。", + "image": "三年克之,惫也。" + }, + "4": { + "name": "六四", + "text": "繻有衣袽,终日戒。", + "image": "终日戒,有所疑也。" + }, + "5": { + "name": "九五", + "text": "东邻杀牛,不如西邻之禴祭,实受其福。", + "image": "东邻杀牛,不如西邻之时也。实受其福,吉大来也。" + }, + "6": { + "name": "上六", + "text": "濡其首,厉。", + "image": "濡其首厉,何可久也。" + } + } + }, + "(0, 1, 0, 1, 0, 1)": { + "name": "未济", + "text": "亨,小狐汔济,濡其尾,无攸利。", + "image": "火在水上,未济;君子以慎辨物居方。", + "tuan": "亨;柔得中也。小狐汔济,未出中也。濡其尾,无攸利;不续终也。虽不当位,刚柔应也。", + "lines": { + "1": { + "name": "初六", + "text": "濡其尾,吝。", + "image": "濡其尾,亦不知极也。" + }, + "2": { + "name": "九二", + "text": "曳其轮,贞吉。", + "image": "九二贞吉,中以行正也。" + }, + "3": { + "name": "六三", + "text": "未济,征凶,利涉大川。", + "image": "未济征凶,位不当也。" + }, + "4": { + "name": "九四", + "text": "贞吉,悔亡,震用伐鬼方,三年有赏于大国。", + "image": "贞吉悔亡,志行也。" + }, + "5": { + "name": "六五", + "text": "贞吉,无悔,君子之光,有孚,吉。", + "image": "君子之光,其晖吉也。" + }, + "6": { + "name": "上九", + "text": "有孚于饮酒,无咎,濡其首,有孚失是。", + "image": "饮酒濡首,亦不知节也。" + } + } + } + } +} \ No newline at end of file diff --git a/next/docs/evidence/stage-11/acceptance.md b/next/docs/evidence/stage-11/acceptance.md new file mode 100644 index 0000000..05f6004 --- /dev/null +++ b/next/docs/evidence/stage-11/acceptance.md @@ -0,0 +1,49 @@ +# 阶段11验收记录 + +## 完成范围 + +- 观势:股票代码或名称载入、正式日线与盘中实时模式、六爻确定性公式、安全门、客观行业数据补录和失败关闭。 +- 观气:基于`lunar-python`的确定性历法计算、固定100分权重、三层气机、个人合参与五行行业折叠区。 +- 观心:40秒呼吸、一次投掷只生成一爻、六次成卦、铜钱正反面、重置与历史。 +- 历史:三种模式均按账号隔离;当天已完成解运直接复用,不重复调用LLM或扣减额度。 +- 解读:统一使用既有`LLMGateway`,保留问天专属循环加载动画和长方形结果弹窗。 +- 前端:PC与移动端、日间与夜间、会员可用态和非会员同结构灰色锁定态。 + +## 关键行为 + +- 观势历史日期只使用正式日线;今日盘中只使用实时行情;收盘后正式日线落库前只接受同日15:00及以后的实时快照。 +- 股票、申万二级行业、行业成分股和三大指数缺失或过期时失败关闭,不生成模拟数据,也不允许用户直接修改阴阳爻。 +- 用户补录的是缺失的客观行业行情字段;通过安全门后再由唯一公式生成卦象。 +- 观气的历法、五运六气和基础断语不由LLM计算;LLM只解释已生成的结构化结果。 +- 观心未投掷的爻只显示“未得”,投掷一次只显现一爻;全部六爻完成前不能解卦。 +- 原始出生日期、时辰和性别不在问天响应或页面中回显。 + +## 自动验收 + +- Ruff:通过。 +- Pytest:89项通过。 +- Vue TypeScript:通过。 +- Vitest:3个文件、7项通过。 +- Vite生产构建:通过。 +- Playwright:阶段4至11共12项通过;阶段11专属2项通过。 +- 数据库:版本9前进、回退和每日解运并发唯一性测试通过。 +- Git差异检查:通过。 +- 密钥扫描:已提供的账号密码和Token无匹配。 + +## 视觉证据 + +- `heaven-dark-1920x1080.jpg` +- `heaven-light-390x844.jpg` +- `heaven-interpret-loading-dark.jpg` + +## 减法审计 + +- 未导入旧`heaven_agent.py`、`heaven_engine.py`、旧页面脚本或旧样式覆盖层。 +- 行情情绪计算从`features/market`迁入唯一数据层,问势实时组装从通用`DataGateway`拆为职责明确的`data/heaven.py`。 +- 三种解读继续复用唯一LLM网关、配额、流协议、弹窗和错误外壳,没有第二套实现。 +- 问天页面按观势、观气、观心拆分,三份页面样式各自受控;最大页面样式408行,超出400行建议值8行,原因是保留已验收的星空、六爻、铜钱和响应式动画规则,不再增加覆盖层。 + +## 残余边界 + +- 生产数据能否成卦取决于正式Tushare权限、盘中实时接口的新鲜度和申万二级成分覆盖;质量门不会静默放宽。 +- NAS生产容器保持不变,最终切换仍需人工确认。 diff --git a/next/docs/evidence/stage-11/heaven-dark-1920x1080.jpg b/next/docs/evidence/stage-11/heaven-dark-1920x1080.jpg new file mode 100644 index 0000000000000000000000000000000000000000..afe51b982e0aeba07a2828ede39da707dc5ef57b GIT binary patch literal 99400 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 zⅆ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 z
I;JjhT+StMFN_YPd{`$leaMZ=Z=747r$;fUX=M^V{4|=1z+Im>=$Zm`Mgg3j< zs{`P^1=!Y1ol8l3YXyL!x=T}M9~ijKLs*w}%NSdTf!I?tSW4~xpU}2waoiKxe)PGv zaH+zo0Dq0kEy`j(dU^t>0x|w9gxD6Yi5w#{P^h?y>xc*cya@*D*V_F z;kI@CVTwf`pW$_~5nA3U)!0*Pk5OLGZm||Cy|(t*=G{UG$-f3{D1CbV>(~-}zst_y z<%HBnxlUL1xFtcOhsC8wGuU4M*DY}m=RgCl-p6j!1tjRdePjEIlzIT{i*t=Ffxi|D ziKU7)H<&(R@QQZZYYNxX#t z+=@XwR>w_o>DBEaCp!__OCR8n^{IMR_Zr27>SIrbx>_lITa@~r`R~!2kQOlh<>1;e zC%aFLEo|L`vDF26hNx40JnpO7UvBZd48MDPUI)L=Mxh84NTaWdOH0BiX=eC{b&M&D zOYx#uiI=Qk@PQ2Ws?zF8iR%c)AE8H@7%d3ZHr;yeaNRjYvj*>|U5Z$TSqR3-q+YvR zsOfee7!>NI1vXH#kct?+X3qf^^zaBJJ>u?HiT>ydH`DZ{Kp{OMiV)aA`L&KPE}1I) z#KP&@PbOQfXuz?{9H%_K9G?NuuI4jncFAU?@~kplIr*#^i21xGq^LO8Z~8n+$g9mn z0#&`*-W*!f6a>a_qf~afWw?}v{VC=8d0}B8IncR6g(Y^4daMP^Zfzy7%G>qQ%cLam z8Ssgfv)*QIvPj7?o1Emu$IbjmJ$Qi<UyfPuW^8;;^qMaSM$Fy|vzWzgBQffw;I| z%|~BQ?d{W-$~YWuEmLFVrEp)H<cAs93{tYt6r|x}EAv!=Un$Kv(Iv?F7eI_zEETYPlcf6RXd0wqQMOCSyozYc|Eo`8*c zQ?i@-PRFIGIIh*}!H% zNq?Ji0^(B&0BpS#>s5&)5cD4vHT*;1#<%KyY&mYYfW%vTrzmQfy*`V{>n-50GTci^ zX!<@I#p$vynI*e08B^;*%H(EQyX(!IJLF}nhhmd1WbniX8Ts{y%CoDeJyP_b#vSv^ z%0XdHdPu2U#bUdpvtW?5i$oE|A}>LI9j1WDI^lWQiH@o{;RaCpU-sbq4Hx#$D`J1f zfJF~nedz%>?3>Q~Mh|d`>hOJ+6Yai12bLIW^tbJw)OYB>9K!m2e}VrF9T>3u*hBnB z4O{l{QHu({I5G+OhxHl?Z}~^gpyK4C9rlmf92BdnXr=LM1Sg*K5dGxIK1;kWb=IpH}UH}l9`{#Oc zubuIDVZmx3zPbR-p<*c@rp>lToYDT zR^SbDLJNbQX90pVa5E@C7HDM+Js=6ORW|+(3tRUJD0}3Vx)|o^1D=!jTBeW2fiy7) zXHJq5lEhMva33v0f=(LsHjTGFDnJLcdk ztK-^{I$-&WjgU8FjGvmEh(}%d@r@>9_nS{Htz{(N~vKgm(4xbeH5Sl~yhc|U*%kM9NSVaANRrnQ~O ztitXd<+TVHN8(T&zirp0H7+=(!1g}0)~Qhw zlPL*HJbhLX#wX%1QXwmUc2sCV5S%tupEM(CrN>3OM9yB0)klcC``vUR(u0mKpHrF@ z<{+B8cLISotmISfJiT3{$EQDaekF>3oyrN?Y+D#|r4j0imV`hlw8pPeuD>rv=u$d6 zh{yI5YyNZbq~9VEe)k?c{@)G%#uGe4{Pf>f2=IgpaO#@eTLW?$$3$0vZzX^H!V)A& zK+9mO3X;3;4X(D^_?cgbgyC@++i^M=lIP|jSe zC1K?jwg^icf=^w}L*(s8tfJEfXYo zlalb5es!m(NOk`O@*DoWoc3lF_WBhWi1C{;Zc5$(!kvB~pJ=%jx5ZKIaeXB7NNTp0 zP2RAoDR@ZHfZgdk`uqc(_?s0&_;*rg3YY8a_2qQ6G}#d$d)+SR&bw>*BoC=t&6||= z$ENIlF6WA&`}`oILdn{ex6_b2Y$_xhd65L@%1nyv&td@#;t47F=B{1u*44+)7NHtY zKOKxbJqHuGb)u{`bX1Rj05LIsrm2#8VI`<1QuN+W+{x{LD@B)y|II`G&*ve7UzS4x zuDyR}ldd0t5aex$BS#%dN7b8kv6z%ieUiu=nq<;@_5MrDQg?6jNl>!NRNB0`@0wI`xG9mo!bn!pyEcHHn0IWCco|&r3 z4H&@=(uB8%EKKa2S(${73bMYrjFJ8Cfq&YE^i+$n&|cPT>3r74q#V{ zMt{#1b?#&KI-2*0UOS&lg*-7kO>q_`U|_PmFISiWFHQ0jFk`$Zs(l5tK`1O{KgEVJM+jd6 z4TvCV>x)4UANk6(4_RE19ce$;A5ERMR}X@iUe-uUkyY;(H?bs-Vzib%oN>8NhihyN z)pe@l9+D#!{K>LH-n+$!*I3t9oRKwPfp>N0ZpoAipR4G;xVd9*(br4M~*d^g#QQ4G>$ z5Sf5=Urn=&5nk#4>IjfKeM)23N)QEzcl2KL%^=JstNX@ zW(m@s#E->+f9z_cDIa(@q{w>lQxblXj#S}A*5NiUr)Nr=N2e%N=n@OZe6ETb0ehoE;hNfEJnCLjDziIM0H%jh2 zx|t<7@yh04Zfpsp^mDYHZyVBN<*kdd^1wYQulV+ASy^xR2gV9ieC6n95fu8PJC7hB z`^0l6blUC7?3Iff5IXs?Jt}|0>{b@H* z8TGY&aJ?ZL>Z-Ka-s-1%hALdK+w)-v6Nblrb5icb;pe3tqAguwoVdd7(?08`hd^Ui zN5ceCYE`L!+Gyf6Hx?F_E&blpKu|2mP@1sR@Ej(Gqu6xCYb<#MB$nW4O$b^YpitENuz}H3PSc zqk8ql#E9UbrKH*BnfbbT13J=hTg+u~f@t69w>t5+|0t;(B6TbhHmc=xU}u_(3o`YW zTpbq1rgVE0Q~Wm`^hon!>P;Ynvz#t*#Z;)pk-8q24{V^SL-CpxPqO3U>d5}* z{e!RfAKur>KjpuaaJ&yP{CPpIl9+iWz_cLd7G;_JuNo?CHybT$HrpI!y=g;C6#(wu%)bfv?~ zVa9e*A%uC6GizPZVC+ZztMr{?p9Z^@^QLCHqNA{>SPvR|@a8%S?u_YZy!%QvoDG^> cQ(IA*)x_EPr>XrBc)$6xs$acC`
+ diff --git a/next/frontend/public/heaven-loading.js b/next/frontend/public/heaven-loading.js new file mode 100644 index 0000000..68d12b8 --- /dev/null +++ b/next/frontend/public/heaven-loading.js @@ -0,0 +1,723 @@ +(function exposeHeavenLoading(global) { + "use strict"; + + // Theme palettes share the original animation geometry and timing. + const LOADING_PALETTES = { + dark: { + paper: "#05060d", + paperCenter: "#10142a", + paperMiddle: "#0b0e1e", + nodeText: "#f7e3b4", + ink: "#e6c37a", + inkBright: "#f7e3b4", + gold: "#e6c37a", + goldBright: "#f7e3b4", + cinnabar: "#d8564a", + dim: "rgba(216,205,180,0.55)", + particles: ["#e6c37a", "#d8564a", "#6d7fa8"], + }, + light: { + paper: "#eef1f4", + paperCenter: "#fffefa", + paperMiddle: "#f4f2eb", + nodeText: "#493a20", + ink: "#8a641d", + inkBright: "#624612", + gold: "#946b1d", + goldBright: "#765315", + cinnabar: "#b94f46", + dim: "rgba(52,58,67,0.62)", + particles: ["#946b1d", "#b94f46", "#73859c"], + }, + }; + let PAPER; + let PAPER_CENTER; + let PAPER_MIDDLE; + let NODE_TEXT; + let INK; + let INK_BRIGHT; + let GOLD; + let GOLD_BRIGHT; + let CINNABAR; + let DIM; + let PARTICLE_COLORS; + const applyLoadingPalette = () => { + const theme = document.documentElement.dataset.theme === "light" ? "light" : "dark"; + const palette = LOADING_PALETTES[theme]; + PAPER = palette.paper; + PAPER_CENTER = palette.paperCenter; + PAPER_MIDDLE = palette.paperMiddle; + NODE_TEXT = palette.nodeText; + INK = palette.ink; + INK_BRIGHT = palette.inkBright; + GOLD = palette.gold; + GOLD_BRIGHT = palette.goldBright; + CINNABAR = palette.cinnabar; + DIM = palette.dim; + PARTICLE_COLORS = palette.particles; + return theme; + }; + applyLoadingPalette(); + const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",serif'; + const ELEMENT_COLORS = { + 木: "#4f7a4a", + 火: "#b3483d", + 土: "#96702c", + 金: "#70685b", + 水: "#496d92", + }; + const QI6 = [ + { name: "厥阴风木", element: "木" }, + { name: "少阴君火", element: "火" }, + { name: "少阳相火", element: "火" }, + { name: "太阴湿土", element: "土" }, + { name: "阳明燥金", element: "金" }, + { name: "太阳寒水", element: "水" }, + ]; + const STEP_RANGES = ["大寒 — 春分", "春分 — 小满", "小满 — 大暑", "大暑 — 秋分", "秋分 — 小雪", "小雪 — 大寒"]; + const TRIGRAMS = [ + { name: "乾", bits: [1, 1, 1], angle: -90 }, + { name: "兑", bits: [1, 1, 0], angle: -135 }, + { name: "离", bits: [1, 0, 1], angle: 180 }, + { name: "震", bits: [1, 0, 0], angle: 135 }, + { name: "巽", bits: [0, 1, 1], angle: -45 }, + { name: "坎", bits: [0, 1, 0], angle: 0 }, + { name: "艮", bits: [0, 0, 1], angle: 45 }, + { name: "坤", bits: [0, 0, 0], angle: 90 }, + ]; + const SIXIANG = [ + { name: "太阳", bits: [1, 1], dx: 0, dy: -1 }, + { name: "少阴", bits: [1, 0], dx: 1, dy: 0 }, + { name: "太阴", bits: [0, 0], dx: 0, dy: 1 }, + { name: "少阳", bits: [0, 1], dx: -1, dy: 0 }, + ]; + const HEXAGRAM_NAMES = [ + "坤", "剥", "比", "观", "豫", "晋", "萃", "否", "谦", "艮", "蹇", "渐", "小过", "旅", "咸", "遁", + "师", "蒙", "坎", "涣", "解", "未济", "困", "讼", "升", "蛊", "井", "巽", "恒", "鼎", "大过", "姤", + "复", "颐", "屯", "益", "震", "噬嗑", "随", "无妄", "明夷", "贲", "既济", "家人", "丰", "革", "同人", "临", + "损", "节", "中孚", "归妹", "睽", "兑", "履", "泰", "大畜", "需", "小畜", "大壮", "大有", "夬", "乾", + ]; + const HEX_TOTAL = 12500; + const FORTUNE_TOTAL = 12800; + const HEX_STAGES = [ + [0, 1800, "太 极", "无极而太极,动而生阳"], + [1800, 3300, "两 仪", "一阴一阳之谓道"], + [3300, 4700, "四 象", "阴阳消长,太少相生"], + [4700, 6800, "八 卦", "天地定位,山泽通气"], + [6800, 10800, "六 十 四 卦", "卦者挂也,悬物象以示人"], + [10800, HEX_TOTAL, "归 一", "万物负阴而抱阳,冲气以为和"], + ]; + const clamp01 = (value) => Math.max(0, Math.min(1, value)); + const smooth = (start, end, value) => { + const progress = clamp01((value - start) / Math.max(1, end - start)); + return progress * progress * (3 - 2 * progress); + }; + const easeOut = (value) => 1 - Math.pow(1 - clamp01(value), 3); + const hexBits = (index) => Array.from({ length: 6 }, (_, bit) => (index >> (5 - bit)) & 1); + const point = (cx, cy, radius, degrees) => { + const radians = degrees * Math.PI / 180; + return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius]; + }; + + class HeavenLoadingCanvas { + constructor(canvas) { + this.canvas = canvas; + this.context = canvas.getContext("2d"); + this.width = 0; + this.height = 0; + this.dpr = 1; + this.scene = "hexagram"; + this.data = {}; + this.startedAt = 0; + this.frameId = 0; + this.running = false; + this.completingAt = 0; + this.completionResolve = null; + this.completionTimer = 0; + this.resizeObserver = new ResizeObserver(() => this.resize()); + this.reducedMotion = global.matchMedia("(prefers-reduced-motion: reduce)").matches; + this.theme = document.documentElement.dataset.theme || "dark"; + this.stars = this.createStars(this.reducedMotion ? 48 : 150); + } + + createStars(count) { + let seed = 24681357; + const random = () => { + seed = (seed * 1664525 + 1013904223) >>> 0; + return seed / 4294967296; + }; + return Array.from({ length: count }, () => ({ + x: random(), + y: random(), + radius: 0.3 + random() * 1.3, + phase: random() * Math.PI * 2, + speed: 0.00015 + random() * 0.0004, + colorIndex: Math.floor(random() * PARTICLE_COLORS.length), + })); + } + + start(scene, data = {}) { + this.theme = applyLoadingPalette(); + const nextScene = scene === "fortune" ? "fortune" : "hexagram"; + if (this.running && this.scene === nextScene) { + this.data = data; + return; + } + this.stop(); + this.scene = nextScene; + this.data = data; + this.startedAt = performance.now(); + this.running = true; + this.canvas.dataset.scene = this.scene; + this.canvas.dataset.running = "true"; + this.canvas.dataset.looping = "true"; + this.resizeObserver.observe(this.canvas); + this.resize(); + if (this.reducedMotion) { + this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now()); + } else { + this.frameId = requestAnimationFrame((now) => this.frame(now)); + } + } + + complete() { + if (!this.running || this.reducedMotion) { + this.stop(); + return Promise.resolve(); + } + if (this.completionResolve) return this.completionPromise; + this.completingAt = performance.now(); + this.completionPromise = new Promise((resolve) => { this.completionResolve = resolve; }); + this.completionTimer = global.setTimeout(() => this.stop(), 2200); + return this.completionPromise; + } + + stop() { + if (this.frameId) cancelAnimationFrame(this.frameId); + this.frameId = 0; + this.running = false; + this.completingAt = 0; + if (this.completionTimer) global.clearTimeout(this.completionTimer); + this.completionTimer = 0; + this.resizeObserver.disconnect(); + this.canvas.dataset.running = "false"; + this.canvas.dataset.looping = "false"; + if (this.completionResolve) this.completionResolve(); + this.completionResolve = null; + this.completionPromise = null; + } + + resize() { + const rect = this.canvas.getBoundingClientRect(); + const width = Math.max(1, Math.round(rect.width)); + const height = Math.max(1, Math.round(rect.height)); + if (width === this.width && height === this.height) return; + this.width = width; + this.height = height; + this.dpr = Math.min(global.devicePixelRatio || 1, 2); + this.canvas.width = Math.round(width * this.dpr); + this.canvas.height = Math.round(height * this.dpr); + this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); + if (this.running && this.reducedMotion) { + this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now()); + } + } + + frame(now) { + if (!this.running) return; + if (this.completingAt) { + const duration = this.scene === "fortune" ? 1800 : 1700; + const progress = clamp01((now - this.completingAt) / duration); + this.drawCompletion(progress, now); + if (progress >= 1) { + this.stop(); + return; + } + } else { + const total = this.scene === "fortune" ? FORTUNE_TOTAL : HEX_TOTAL; + const elapsed = Math.max(0, now - this.startedAt); + const timeline = elapsed % total; + this.canvas.dataset.cycle = String(Math.floor(elapsed / total)); + this.draw(timeline, now); + } + this.frameId = requestAnimationFrame((time) => this.frame(time)); + } + + draw(time, now) { + if (this.width <= 1 || this.height <= 1) return; + this.drawBackground(now); + if (this.scene === "fortune") this.drawFortune(time, now); + else this.drawHexagram(time, now); + } + + drawBackground(now) { + const currentTheme = document.documentElement.dataset.theme || "dark"; + if (currentTheme !== this.theme) this.theme = applyLoadingPalette(); + const { context: ctx, width, height } = this; + const cx = width / 2; + const cy = height * 0.4; + const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.75); + gradient.addColorStop(0, PAPER_CENTER); + gradient.addColorStop(0.52, PAPER_MIDDLE); + gradient.addColorStop(1, PAPER); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, width, height); + for (const star of this.stars) { + const twinkle = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(star.phase + now * 0.0012)); + const alpha = twinkle * 0.5; + ctx.globalAlpha = alpha; + ctx.fillStyle = PARTICLE_COLORS[star.colorIndex]; + const y = ((star.y + now * star.speed) % 1) * height; + ctx.fillRect(star.x * width, y, star.radius, star.radius); + } + ctx.globalAlpha = 1; + } + + label(text, x, y, size, color = INK, alpha = 1, weight = "", maxWidth) { + if (!text || alpha <= 0) return; + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.fillStyle = color; + ctx.font = `${weight ? `${weight} ` : ""}${size}px ${SERIF}`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + if (maxWidth) ctx.fillText(text, x, y, maxWidth); + else ctx.fillText(text, x, y); + ctx.restore(); + } + + node(x, y, radius, color, alpha = 1, glow = 0) { + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.fillStyle = color; + ctx.shadowColor = color; + ctx.shadowBlur = glow; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } + + line(x1, y1, x2, y2, color, alpha = 1, width = 1) { + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.strokeStyle = color; + ctx.lineWidth = width; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + ctx.restore(); + } + + curvedArrow(x1, y1, x2, y2, mx, my, color, alpha) { + if (alpha <= 0) return; + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.strokeStyle = color; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.quadraticCurveTo(mx, my, x2, y2); + ctx.stroke(); + const angle = Math.atan2(y2 - my, x2 - mx); + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(x2, y2); + ctx.lineTo(x2 - 7 * Math.cos(angle - 0.42), y2 - 7 * Math.sin(angle - 0.42)); + ctx.lineTo(x2 - 7 * Math.cos(angle + 0.42), y2 - 7 * Math.sin(angle + 0.42)); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + + drawYao(cx, cy, width, lineWidth, yang, alpha, glow = 0) { + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.fillStyle = INK; + ctx.shadowColor = GOLD; + ctx.shadowBlur = glow; + if (yang) { + ctx.fillRect(cx - width / 2, cy - lineWidth / 2, width, lineWidth); + } else { + const gap = width * 0.18; + ctx.fillRect(cx - width / 2, cy - lineWidth / 2, (width - gap) / 2, lineWidth); + ctx.fillRect(cx + gap / 2, cy - lineWidth / 2, (width - gap) / 2, lineWidth); + } + ctx.restore(); + } + + drawGua(cx, cy, width, lineWidth, bits, alpha, glow = 0) { + const gap = lineWidth * 1.7; + const top = cy - (bits.length - 1) * gap / 2; + bits.forEach((bit, index) => { + this.drawYao(cx, top + (bits.length - 1 - index) * gap, width, lineWidth, bit === 1, alpha, glow); + }); + } + + stageAlpha(time, start, end, fade = 300, hold = false) { + const enter = smooth(start, start + fade, time); + return hold ? enter : enter * (1 - smooth(end - fade, end, time)); + } + + fortuneStages() { + const sixQi = this.data.sixQi || {}; + const pillar = this.data.yearPillar || "岁运"; + const movement = this.data.movement || "中运合参"; + const sitian = sixQi.sitian || "司天气候"; + return [ + [0, 2100, "五 运", "木火土金水,五运相袭,周而复始"], + [2100, 3900, "十 干 化 运", "甲己土 · 乙庚金 · 丙辛水 · 丁壬木 · 戊癸火"], + [3900, 5800, "十 二 支 化 气", "子午少阴 · 丑未太阴 · 寅申少阳 · 卯酉阳明 · 辰戌太阳 · 巳亥厥阴"], + [5800, 7900, "六 气 环 布", "风寒暑湿燥火,分主六步,以应岁时"], + [7900, 11000, "岁 运 合 参", `${pillar}年 · 中运${movement} · ${sitian}司天`], + [11000, FORTUNE_TOTAL, "归 一", "谨守病机,无失气宜"], + ]; + } + + drawFooter(time, now, total, stages, scene) { + const { context: ctx, width, height } = this; + const stage = [...stages].reverse().find((item) => time >= item[0]) || stages[0]; + const labelAlpha = smooth(stage[0], stage[0] + 300, time) + * (1 - smooth(stage[1] - 250, stage[1], time)); + this.label(stage[2], width / 2, height - 108, 19, GOLD, 0.55 + 0.45 * labelAlpha, "600"); + this.label(stage[3], width / 2, height - 84, 12.5, DIM, (0.4 + 0.4 * labelAlpha) * (scene === "fortune" ? 0.85 : 0.8), "", width - 32); + + const baseSlotWidth = 34; + const baseSlotHeight = 5; + const baseSlotGap = 12; + const baseTotalWidth = baseSlotWidth * 6 + baseSlotGap * 5; + const fit = Math.min(1, (width - 28) / baseTotalWidth); + const slotWidth = baseSlotWidth * fit; + const slotHeight = baseSlotHeight * fit; + const slotGap = baseSlotGap * fit; + const totalWidth = slotWidth * 6 + slotGap * 5; + const filled = Math.min(6, Math.floor(time / (total / 6))); + for (let index = 0; index < 6; index += 1) { + const x = width / 2 - totalWidth / 2 + index * (slotWidth + slotGap); + const y = height - 56; + const color = scene === "fortune" ? ELEMENT_COLORS[QI6[index].element] : GOLD; + ctx.save(); + ctx.globalAlpha = 0.16; + ctx.strokeStyle = GOLD; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, slotWidth, slotHeight); + ctx.restore(); + if (index < filled) { + ctx.save(); + ctx.globalAlpha = 0.9; + ctx.fillStyle = color; + ctx.shadowColor = color; + ctx.shadowBlur = 8; + ctx.fillRect(x, y, slotWidth, slotHeight); + ctx.restore(); + } else if (index === filled) { + ctx.save(); + ctx.globalAlpha = 0.35 + 0.3 * Math.sin(now / 200); + ctx.fillStyle = color; + const progress = (time % (total / 6)) / (total / 6); + ctx.fillRect(x, y, slotWidth * progress, slotHeight); + ctx.restore(); + } + } + const dots = ".".repeat(1 + Math.floor(now / 450) % 3); + const loadingText = scene === "fortune" ? "推 演 运 气 · 加 载 中" : "推 演 天 机 · 加 载 中"; + this.label(`${loadingText}${dots}`, width / 2, height - 32, 13, GOLD, 0.75); + } + + drawTrigramRing(cx, cy, radius, width, lineWidth, alpha, now, entering, time) { + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha * 0.13; + ctx.strokeStyle = GOLD; + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + const breath = 1 + 0.006 * Math.sin(now / 620); + TRIGRAMS.forEach((trigram, index) => { + const progress = entering ? easeOut((time - 4700 - index * 130) / 700) : 1; + if (progress <= 0) return; + const [x, y] = point(cx, cy, radius * breath * progress, trigram.angle); + this.drawGua(x, y, width, lineWidth, trigram.bits, alpha * progress, alpha * progress * 8); + const nameAlpha = entering ? alpha * clamp01((time - 4700 - index * 130 - 480) / 500) : alpha; + this.label(trigram.name, x, y + lineWidth * 5.2, 13, GOLD, nameAlpha * (0.55 + 0.2 * Math.sin(now / 700 + index))); + }); + } + + drawHexagram(time, now) { + const { width, height } = this; + const cx = width / 2; + const cy = height * 0.4; + const scale = Math.min(width, Math.max(1, height - 150)); + if (time < 1800) { + const alpha = this.stageAlpha(time, 0, 1800); + this.node(cx, cy, 5.5 * (1 + 0.12 * Math.sin(now / 260)), GOLD_BRIGHT, alpha, 34); + for (let ring = 0; ring < 3; ring += 1) { + const progress = ((now / 1500) + ring / 3) % 1; + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = (1 - progress) * 0.22 * alpha; + ctx.strokeStyle = GOLD; + ctx.beginPath(); + ctx.arc(cx, cy, 8 + progress * scale * 0.13, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + } + if (time >= 1800 && time < 3300) { + const alpha = this.stageAlpha(time, 1800, 3300); + const progress = easeOut((time - 1850) / 850); + const yaoWidth = scale * 0.19 * progress; + const yaoLine = Math.max(scale * 0.013, 5); + this.drawYao(cx, cy - yaoLine * 2.6, yaoWidth, yaoLine, true, alpha, 14); + this.drawYao(cx, cy + yaoLine * 2.6, yaoWidth, yaoLine, false, alpha, 14); + this.node(cx, cy, 4, GOLD_BRIGHT, alpha * (1 - progress) * 0.9); + } + if (time >= 3300 && time < 4700) { + const alpha = this.stageAlpha(time, 3300, 4700); + const distance = scale * 0.085; + const yaoWidth = Math.max(scale * 0.055, 28); + const yaoLine = Math.max(scale * 0.009, 3.5); + SIXIANG.forEach((symbol, index) => { + const progress = easeOut((time - 3330 - index * 160) / 520); + if (progress <= 0) return; + const x = cx + symbol.dx * distance; + const y = cy + symbol.dy * distance; + this.drawGua(x, y, yaoWidth * progress, yaoLine, symbol.bits, alpha * progress, 10); + this.label(symbol.name, x, y + yaoLine * 5.4, 12, GOLD, alpha * progress * 0.55); + }); + } + const trigramRadius = scale * 0.215; + const trigramWidth = Math.max(scale * 0.052, 26); + const trigramLine = Math.max(scale * 0.0075, 3); + if (time >= 4700 && time < 6800) { + this.drawTrigramRing(cx, cy, trigramRadius, trigramWidth, trigramLine, this.stageAlpha(time, 4700, 6800), now, true, time); + } + if (time >= 6800 && time < 10800) { + const alpha = this.stageAlpha(time, 6800, 10800, 350); + this.drawTrigramRing(cx, cy, trigramRadius, trigramWidth * 0.85, trigramLine * 0.85, alpha * 0.42, now, false, time); + const ringRadius = scale * 0.365; + const hexWidth = Math.max(scale * 0.026, 13); + const hexLine = Math.max(scale * 0.0042, 1.6); + const count = Math.floor(clamp01((time - 7000) / 3600) * 64); + for (let index = 0; index < 64; index += 1) { + const [x, y] = point(cx, cy, ringRadius, -90 + index * 360 / 64); + this.node(x, y, 1.4, GOLD, alpha * 0.14); + if (index < count) { + const freshness = Math.max(0, 1 - (count - 1 - index) / 5); + if (freshness > 0) { + const ctx = this.context; + const gradient = ctx.createLinearGradient(cx, cy, x, y); + gradient.addColorStop(0, "rgba(230,195,122,0)"); + gradient.addColorStop(1, GOLD); + this.line(cx, cy, x, y, gradient, alpha * freshness * 0.35); + } + this.drawGua(x, y, hexWidth, hexLine, hexBits(index), alpha * (0.55 + 0.45 * freshness), freshness * 9); + } + } + if (count > 0) { + const current = count - 1; + const popTime = clamp01((time - (7000 + current * 3600 / 64)) / 130); + const pop = 1 + 0.22 * (1 - popTime); + this.drawGua(cx, cy - scale * 0.028, scale * 0.085 * pop, Math.max(scale * 0.011, 4.5), hexBits(current), alpha, 16); + this.label(HEXAGRAM_NAMES[current], cx, cy + scale * 0.062, Math.max(20, scale * 0.042), GOLD_BRIGHT, alpha, "600"); + this.label(`第 ${current + 1} 卦`, cx, cy + scale * 0.105, 13, GOLD, alpha * 0.55); + } + } + if (time >= 10800) { + const alpha = this.stageAlpha(time, 10800, HEX_TOTAL, 420); + const progress = easeOut((time - 10850) / 1150); + const radius = scale * 0.365 * (1 - progress); + for (let index = 0; index < 64 && radius >= 8; index += 1) { + const [x, y] = point(cx, cy, radius, -90 + index * 360 / 64); + this.drawGua(x, y, Math.max(scale * 0.026, 13), Math.max(scale * 0.0042, 1.6), hexBits(index), (1 - progress) * 0.7 * alpha); + } + this.node(cx, cy, 3 + progress * 6, GOLD_BRIGHT, alpha * (0.3 + 0.7 * progress), 12 + progress * 40); + } + this.drawFooter(time, now, HEX_TOTAL, HEX_STAGES, "hexagram"); + } + + drawFortune(time, now) { + const { width, height } = this; + const cx = width / 2; + const cy = height * 0.4; + const scale = Math.min(width, Math.max(1, height - 150)); + if (time < 2100) this.drawFiveMovements(time, now, cx, cy, scale); + if (time >= 2100 && time < 3900) this.drawStems(time, cx, cy, scale); + if (time >= 3900 && time < 5800) this.drawBranches(time, cx, cy, scale); + if (time >= 5800 && time < 7900) this.drawSixQi(time, now, cx, cy, scale); + if (time >= 7900 && time < 11000) this.drawAnnualQi(time, now, cx, cy, scale); + if (time >= 11000) { + const alpha = this.stageAlpha(time, 11000, FORTUNE_TOTAL, 420); + const progress = easeOut((time - 11050) / 1200); + const radius = scale * 0.30 * (1 - progress); + QI6.forEach((qi, index) => { + const [x, y] = point(cx, cy, radius, -90 + index * 60); + if (radius > 8) this.node(x, y, Math.max(scale * 0.011, 6), ELEMENT_COLORS[qi.element], (1 - progress) * 0.8 * alpha, 8); + }); + this.node(cx, cy, 3 + progress * 6, GOLD_BRIGHT, alpha * (0.3 + 0.7 * progress), 12 + progress * 40); + } + this.drawFooter(time, now, FORTUNE_TOTAL, this.fortuneStages(), "fortune"); + } + + drawFiveMovements(time, now, cx, cy, scale) { + const alpha = this.stageAlpha(time, 0, 2100); + const radius = scale * 0.17; + const nodeRadius = Math.max(scale * 0.018, 9); + const elements = [ + ["木", 180], ["火", -90], ["金", 0], ["水", 90], ["土", null], + ]; + const positions = {}; + this.node(cx, cy, 5 + 1.5 * Math.sin(now / 260), GOLD_BRIGHT, alpha * (1 - easeOut((time - 200) / 800)), 30); + elements.forEach(([element, degrees], index) => { + const progress = easeOut((time - 500 - index * 170) / 500); + if (progress <= 0) return; + const x = degrees === null ? cx : cx + Math.cos(degrees * Math.PI / 180) * radius * progress; + const y = degrees === null ? cy : cy + Math.sin(degrees * Math.PI / 180) * radius * progress; + positions[element] = [x, y]; + this.node(x, y, nodeRadius * progress, ELEMENT_COLORS[element], alpha * progress, 16); + this.label(element, x, y + 0.5, Math.round(nodeRadius * 1.15), NODE_TEXT, alpha * progress, "600"); + const direction = element === "土" ? "中央土" : { 木: "东方木", 火: "南方火", 金: "西方金", 水: "北方水" }[element]; + this.label(direction, x, y + nodeRadius + 14, 12, ELEMENT_COLORS[element], alpha * progress * 0.75); + }); + const order = ["木", "火", "土", "金", "水"]; + order.forEach((element, index) => { + const from = positions[element]; + const to = positions[order[(index + 1) % order.length]]; + if (!from || !to) return; + const progress = smooth(1450 + index * 130, 1700 + index * 130, time); + const mx = (from[0] + to[0]) / 2 + (cx - (from[0] + to[0]) / 2) * 0.25; + const my = (from[1] + to[1]) / 2 + (cy - (from[1] + to[1]) / 2) * 0.25; + this.curvedArrow(from[0], from[1], to[0], to[1], mx, my, GOLD, alpha * progress * 0.4); + }); + } + + drawStems(time, cx, cy, scale) { + const alpha = this.stageAlpha(time, 2100, 3900); + const stems = "甲乙丙丁戊己庚辛壬癸"; + const movements = ["土", "金", "水", "木", "火"]; + const radius = scale * 0.30; + for (let index = 0; index < 10; index += 1) { + const progress = smooth(2150 + index * 90, 2450 + index * 90, time); + if (progress <= 0) continue; + const [x, y] = point(cx, cy, radius, -90 + index * 36); + const element = movements[index % 5]; + this.node(x, y, 3, ELEMENT_COLORS[element], alpha * progress, 8); + this.label(stems[index], x, y - 14, 15, ELEMENT_COLORS[element], alpha * progress, "600"); + } + for (let index = 0; index < 5; index += 1) { + const progress = smooth(3150 + index * 110, 3450 + index * 110, time); + const angle = -90 + index * 36; + const [x1, y1] = point(cx, cy, radius, angle); + const [x2, y2] = point(cx, cy, radius, -90 + (index + 5) * 36); + this.line(x1, y1, x2, y2, ELEMENT_COLORS[movements[index]], alpha * progress * 0.45); + const [labelX, labelY] = point(cx, cy, scale * 0.055, angle + 90); + this.label(movements[index], labelX, labelY, 16, ELEMENT_COLORS[movements[index]], alpha * progress, "600"); + } + } + + drawBranches(time, cx, cy, scale) { + const alpha = this.stageAlpha(time, 3900, 5800); + const branches = "子丑寅卯辰巳午未申酉戌亥"; + const qiNames = ["少阴君火", "太阴湿土", "少阳相火", "阳明燥金", "太阳寒水", "厥阴风木"]; + const radius = scale * 0.31; + const branchAngle = (index) => -90 + ((index - 6 + 12) % 12) * 30; + for (let index = 0; index < 12; index += 1) { + const progress = smooth(3950 + index * 70, 4220 + index * 70, time); + const [x, y] = point(cx, cy, radius, branchAngle(index)); + this.node(x, y, 2.5, GOLD, alpha * progress, 6); + this.label(branches[index], x, y - 13, 14, GOLD, alpha * progress * 0.9); + } + qiNames.forEach((name, index) => { + const progress = smooth(4900 + index * 130, 5200 + index * 130, time); + const [x1, y1] = point(cx, cy, radius, branchAngle(index)); + const [x2, y2] = point(cx, cy, radius, branchAngle(index + 6)); + const element = QI6.find((item) => item.name === name)?.element || "土"; + this.line(x1, y1, x2, y2, ELEMENT_COLORS[element], alpha * progress * 0.4); + const [labelX, labelY] = point(cx, cy, radius + scale * 0.055, branchAngle(index)); + this.label(name, labelX, labelY, 12, ELEMENT_COLORS[element], alpha * progress, "600"); + }); + } + + drawSixQi(time, now, cx, cy, scale) { + const alpha = this.stageAlpha(time, 5800, 7900); + const radius = scale * 0.27; + const drift = now * 0.004; + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha * 0.13; + ctx.strokeStyle = GOLD; + ctx.beginPath(); + ctx.arc(cx, cy, radius, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + QI6.forEach((qi, index) => { + const progress = easeOut((time - 5850 - index * 180) / 550); + const [x, y] = point(cx, cy, radius * progress, -90 + index * 60 + drift); + const nodeRadius = Math.max(scale * 0.015, 8) * progress; + this.node(x, y, nodeRadius, ELEMENT_COLORS[qi.element], alpha * progress, 14); + this.label(qi.name, x, y - nodeRadius - 12, 13, ELEMENT_COLORS[qi.element], alpha * progress, "600"); + this.label(["初之气", "二之气", "三之气", "四之气", "五之气", "终之气"][index], x, y + nodeRadius + 12, 10.5, DIM, alpha * progress * 0.9); + }); + this.node(cx, cy, 4 + Math.sin(now / 300), GOLD_BRIGHT, alpha * 0.9, 24); + } + + drawAnnualQi(time, now, cx, cy, scale) { + const alpha = this.stageAlpha(time, 7900, 11000, 350); + const sixQi = this.data.sixQi || {}; + const pillar = this.data.yearPillar || "岁运"; + const movement = this.data.movement || "中运合参"; + const sitian = sixQi.sitian || "司天气候"; + const zaiquan = sixQi.zaiquan || "在泉气化"; + const currentStep = Math.max(1, Math.min(6, Number(sixQi.step) || 1)); + const qiElement = (name) => QI6.find((item) => item.name === name)?.element || "土"; + const movementElement = ["木", "火", "土", "金", "水"].find((element) => movement.includes(element)) || "土"; + this.label("司 天", cx, cy - scale * 0.212, 11, DIM, alpha * smooth(7950, 8450, time)); + this.label(sitian, cx, cy - scale * 0.178, 17, ELEMENT_COLORS[qiElement(sitian)], alpha * smooth(7950, 8450, time), "600"); + this.label(zaiquan, cx, cy + scale * 0.178, 17, ELEMENT_COLORS[qiElement(zaiquan)], alpha * smooth(8200, 8700, time), "600"); + this.label("在 泉", cx, cy + scale * 0.212, 11, DIM, alpha * smooth(8200, 8700, time)); + this.label(pillar, cx, cy - scale * 0.012, Math.max(22, scale * 0.052), GOLD_BRIGHT, alpha * smooth(8500, 9100, time), "600"); + this.label(`${pillar}年 · 中运${movement}`, cx, cy + scale * 0.052, 14, ELEMENT_COLORS[movementElement], alpha * smooth(8500, 9100, time), "600", scale * 0.62); + const radius = scale * 0.30; + QI6.forEach((qi, index) => { + const progress = smooth(9200 + index * 260, 9480 + index * 260, time); + const [x, y] = point(cx, cy, radius, -90 + index * 60); + const current = index + 1 === currentStep; + const pulse = current ? 0.5 + 0.5 * Math.sin(now / 230) : 0; + this.node(x, y, Math.max(scale * 0.011, 6) + (current ? 2.5 : 0), ELEMENT_COLORS[qi.element], alpha * progress, 12 + pulse * 14); + if (current) { + const ctx = this.context; + ctx.save(); + ctx.globalAlpha = alpha * (0.35 + pulse * 0.35); + ctx.strokeStyle = CINNABAR; + ctx.lineWidth = 1.2; + ctx.beginPath(); + ctx.arc(x, y, Math.max(scale * 0.02, 11) + pulse * 3, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + this.label("当今", x, y - Math.max(scale * 0.038, 21), 10.5, CINNABAR, alpha * progress, "600"); + } + const stepName = `${index + 1 === 6 ? "终" : ["初", "二", "三", "四", "五"][index]}之气`; + this.label(`${stepName} · ${qi.name}`, x, y + Math.max(scale * 0.03, 17), 11.5, current ? GOLD_BRIGHT : ELEMENT_COLORS[qi.element], alpha * progress * (current ? 1 : 0.85), current ? "600" : ""); + if (current) this.label(STEP_RANGES[index], x, y + Math.max(scale * 0.052, 33), 10, DIM, alpha * progress); + }); + } + + drawCompletion(progress, now) { + this.drawBackground(now); + if (this.scene === "fortune") { + this.drawFortune(11000 + progress * (FORTUNE_TOTAL - 11000), now); + } else { + this.drawHexagram(10800 + progress * (HEX_TOTAL - 10800), now); + } + } + } + + global.HeavenLoadingCanvas = HeavenLoadingCanvas; +})(window); diff --git a/next/frontend/src/app/views/WorkspaceView.vue b/next/frontend/src/app/views/WorkspaceView.vue index 8e52938..784110f 100644 --- a/next/frontend/src/app/views/WorkspaceView.vue +++ b/next/frontend/src/app/views/WorkspaceView.vue @@ -6,6 +6,7 @@ import EmptyState from "../../shared/components/EmptyState.vue"; import MarketWorkspaceView from "../../pages/market/MarketWorkspaceView.vue"; import ScreenerPage from "../../pages/screener/ScreenerPage.vue"; import MentorPage from "../../pages/mentor/MentorPage.vue"; +import HeavenPage from "../../pages/heaven/HeavenPage.vue"; import { useMarketStore } from "../../shared/stores/market"; import { useSessionStore } from "../../shared/stores/session"; import { useUiStore } from "../../shared/stores/ui"; @@ -33,6 +34,7 @@ const implementedMarket = computed(() => +