rebuild(audit): complete entity detail and market preview

This commit is contained in:
leefer
2026-07-30 09:25:52 +08:00
parent d1b3b977d2
commit 4fc8691eee
27 changed files with 946 additions and 35 deletions
+98
View File
@@ -143,6 +143,86 @@ class MarketSnapshotService:
raise SnapshotSyncError("不支持的市场工作区")
return response
def entity_detail(
self, entity_type: str, identifier: str, requested_date: str | None = None
) -> dict[str, Any]:
context = self._gateway.trade_context(requested_date)
if context.actual_date is None:
raise SnapshotSyncError("等待管理员首次同步真实收盘行情")
entity = self._gateway.resolve_entity(entity_type, identifier)
series = self._gateway.chart(entity_type, entity.identifier, "day")
eligible = [point for point in series.points if point.time <= context.actual_date]
if not eligible:
raise SnapshotSyncError("所选日期之前没有可核验的真实行情")
current = eligible[-1]
previous = eligible[-2].close if len(eligible) > 1 else series.previous_close
change = (current.close / previous - 1) * 100 if previous else None
with self._database.read() as connection:
summary = self._repository.latest_summary(connection, context.actual_date)
factor_row = connection.execute(
"""
SELECT factor_values.payload_json
FROM screener_factor_values AS factor_values
JOIN screener_factor_snapshots AS snapshots
ON snapshots.id = factor_values.snapshot_id
WHERE factor_values.identifier = ? AND snapshots.trade_date <= ?
ORDER BY snapshots.trade_date DESC, snapshots.id DESC LIMIT 1
""",
(entity.identifier, context.actual_date),
).fetchone()
factors = json.loads(str(factor_row["payload_json"])) if factor_row else {}
snapshot = json.loads(str(summary["payload_json"])) if summary else {}
event = (
_entity_event(snapshot, entity.identifier, entity.code)
if entity_type == "stock"
else None
)
metrics = [
{"key": "open", "label": "开盘", "value": current.open, "unit": ""},
{"key": "high", "label": "最高", "value": current.high, "unit": ""},
{"key": "low", "label": "最低", "value": current.low, "unit": ""},
{"key": "amount", "label": "成交额", "value": current.amount, "unit": ""},
{"key": "volume", "label": "成交量", "value": current.volume, "unit": ""},
]
for key, label, unit in (
("turnover_rate", "换手率", "%"),
("return_5d", "近5日", "%"),
("return_20d", "近20日", "%"),
("total_mv_billion", "总市值", "亿"),
("circ_mv_billion", "流通市值", "亿"),
):
metrics.append({"key": key, "label": label, "value": factors.get(key), "unit": unit})
money_flow = None
if entity_type == "stock":
money_flow = {
"available": any(
factors.get(key) is not None
for key in ("net_flow_million", "large_flow_million", "net_flow_5d_million")
),
"net_million": factors.get("net_flow_million"),
"large_million": factors.get("large_flow_million"),
"net_5d_million": factors.get("net_flow_5d_million"),
"flow_to_circ_mv_5d": factors.get("flow_to_circ_mv_5d"),
}
return {
"entity": {
"entity_type": entity.entity_type,
"identifier": entity.identifier,
"code": entity.code,
"name": factors.get("name") or entity.name,
"sector": factors.get("sector") or entity.sector,
},
"trade_date": current.time,
"observed_at": series.metadata.observed_at,
"price": current.close,
"previous_close": previous,
"change": round(change, 4) if change is not None else None,
"metrics": metrics,
"money_flow": money_flow,
"event": event,
}
def rotation_member_target(
self, requested_date: str | None, sector_name: str
) -> tuple[str, str]:
@@ -185,6 +265,24 @@ def _history_item(payload: dict[str, Any]) -> dict[str, Any]:
}
def _entity_event(
snapshot: dict[str, Any], identifier: str, code: str
) -> dict[str, Any] | None:
for key, status in (("limits", "涨停"), ("broken", "炸板"), ("down_limits", "跌停")):
for row in snapshot.get(key) or []:
if str(row.get("identifier") or "") == identifier or str(row.get("code") or "") == code:
return {
"status": status,
"reason": str(row.get("reason") or ""),
"streak": row.get("streak"),
"first_time": row.get("first_time"),
"last_time": row.get("last_time"),
"open_times": row.get("open_times"),
"seal_amount": row.get("seal_amount"),
}
return None
def _date(value: str) -> str:
try:
return date.fromisoformat(value).isoformat()