rebuild(runtime): govern market operations and job truth

This commit is contained in:
leefer
2026-07-30 10:14:29 +08:00
parent 4fc8691eee
commit d8f0dd930c
39 changed files with 2224 additions and 79 deletions
+84 -1
View File
@@ -11,7 +11,8 @@ 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.snapshot import build_snapshot
from backend.features.market.events import apply_event_revisions
from backend.features.market.snapshot import build_realtime_inputs, build_snapshot
SHANGHAI = ZoneInfo("Asia/Shanghai")
@@ -94,6 +95,79 @@ class MarketSnapshotService:
"limit_down": len(snapshot["down_limits"]),
"broken": len(snapshot["broken"]),
"temperature": sentiment["score"],
"source_set": ["tushare", "local"],
"output_version": "market-summary-v1",
}
def sync_realtime(
self, requested_date: str | None = None, now: datetime | None = None
) -> dict[str, Any]:
clock = now or datetime.now(SHANGHAI)
target = _date(requested_date or clock.date().isoformat())
if target != clock.date().isoformat():
raise SnapshotSyncError("盘中任务只允许同步当前交易日")
local_time = clock.time().replace(tzinfo=None)
in_window = time(9, 15) <= local_time < time(11, 35) or time(
12, 55
) <= local_time < time(15, 5)
if not in_window:
raise SnapshotSyncError("当前不在盘中行情刷新窗口")
with self._database.read() as connection:
dates = self._repository.open_dates(connection, target, 2)
active_count = self._repository.active_stock_count(connection)
if len(dates) < 2 or dates[0] != target:
raise SnapshotSyncError("当前日期不是有效交易日")
if active_count <= 0:
raise SnapshotSyncError("请先同步股票目录")
try:
raw = self._gateway.realtime_snapshot_inputs(target, dates[1])
except (ProviderError, MarketDataUnavailable) as exc:
raise SnapshotSyncError("盘中行情读取失败,已保留最后成功快照") from exc
daily = raw.get("daily")
price_limits = raw.get("price_limits")
if not isinstance(daily, ProviderResult):
raise SnapshotSyncError("盘中行情缺失,已保留最后成功快照")
coverage = len(daily.rows) / active_count
if coverage < 0.9:
raise SnapshotSyncError(
f"盘中行情覆盖率仅{coverage * 100:.1f}%,已保留最后成功快照"
)
if not isinstance(price_limits, ProviderResult):
raise SnapshotSyncError("盘中涨跌停价格缺失,已保留最后成功快照")
limit_coverage = len(price_limits.rows) / max(len(daily.rows), 1)
if limit_coverage < 0.95:
raise SnapshotSyncError("盘中涨跌停价格覆盖不足,已保留最后成功快照")
inputs = build_realtime_inputs(raw, self._gateway.stock_directory())
snapshot = build_snapshot(target, dates[1], inputs)
with self._database.read() as connection:
rows = self._repository.summaries(connection, dates[1], 250)
history = [json.loads(str(row["payload_json"])) for row in rows]
sentiment = calculate_sentiment(snapshot, history)
snapshot["sentiment"] = sentiment
snapshot.update(snapshot["overview"])
snapshot["temperature"] = sentiment["score"]
observed_at = clock.isoformat(timespec="seconds")
with self._database.transaction() as connection:
self._repository.save_summary(
connection,
trade_date=target,
observed_at=observed_at,
state=SnapshotState.REALTIME.value,
source="tushare",
coverage=min(coverage, 1),
payload=snapshot,
)
return {
"trade_date": target,
"observed_at": observed_at,
"coverage": round(min(coverage, 1), 4),
"stocks": len(daily.rows),
"limit_up": len(snapshot["limits"]),
"limit_down": len(snapshot["down_limits"]),
"broken": len(snapshot["broken"]),
"temperature": sentiment["score"],
"source_set": ["tushare", "local"],
"output_version": "market-summary-realtime-v1",
}
def workspace(self, key: str, requested_date: str | None = None) -> dict[str, Any]:
@@ -104,6 +178,9 @@ class MarketSnapshotService:
if row is None:
return {"trade_date": None, "message": "等待管理员首次同步真实收盘行情"}
payload = json.loads(str(row["payload_json"]))
with self._database.read() as connection:
revisions = self._repository.event_revisions(connection, str(row["trade_date"]))
apply_event_revisions(payload, revisions)
response: dict[str, Any] = {
"trade_date": str(row["trade_date"]),
"observed_at": str(row["observed_at"]),
@@ -173,6 +250,12 @@ class MarketSnapshotService:
).fetchone()
factors = json.loads(str(factor_row["payload_json"])) if factor_row else {}
snapshot = json.loads(str(summary["payload_json"])) if summary else {}
if summary:
with self._database.read() as connection:
revisions = self._repository.event_revisions(
connection, str(summary["trade_date"])
)
apply_event_revisions(snapshot, revisions)
event = (
_entity_event(snapshot, entity.identifier, entity.code)
if entity_type == "stock"