Files
xiaobaifupan/next/backend/features/market/snapshot.py
T

246 lines
8.9 KiB
Python

from __future__ import annotations
from statistics import mean
from typing import Any
from backend.data.contracts import ProviderResult
def build_snapshot(
trade_date: str,
previous_trade_date: str,
inputs: dict[str, ProviderResult | dict[str, Any]],
) -> dict[str, Any]:
daily_rows = _rows(inputs, "daily")
daily = {str(row.get("ts_code") or ""): row for row in daily_rows}
limits = _pool(_rows(inputs, "limit_up"), "涨停")
broken = _pool(_rows(inputs, "broken"), "炸板")
down_limits = _pool(_rows(inputs, "limit_down"), "跌停")
previous_limits = _pool(_rows(inputs, "previous_limit_up"), "涨停")
price_limits = {str(row.get("ts_code") or ""): row for row in _rows(inputs, "price_limits")}
for row in broken:
up_limit = _number(price_limits.get(row["identifier"], {}).get("up_limit"))
row["distance_to_limit"] = (
round((up_limit - row["price"]) / up_limit * 100, 2) if up_limit else None
)
yesterday = _yesterday(
previous_limits,
daily,
limits,
broken,
down_limits,
)
up_count = sum(_number(row.get("pct_chg")) > 0 for row in daily_rows)
down_count = sum(_number(row.get("pct_chg")) < 0 for row in daily_rows)
flat_count = len(daily_rows) - up_count - down_count
amount = sum(_number(row.get("amount")) * 1000 for row in daily_rows)
seal_rate = len(limits) / max(len(limits) + len(broken), 1) * 100
sectors = _sectors(limits)
previous_sectors = _sectors(previous_limits)
overview = {
"up_count": up_count,
"down_count": down_count,
"flat_count": flat_count,
"limit_up": len(limits),
"limit_down": len(down_limits),
"broken": len(broken),
"seal_rate": round(seal_rate, 1),
"amount": round(amount, 2),
}
return {
"trade_date": trade_date,
"previous_trade_date": previous_trade_date,
"overview": overview,
"limits": limits,
"broken": broken,
"down_limits": down_limits,
"yesterday_limits": yesterday,
"limit_performance": _performance(yesterday),
"ladders": _ladders(limits),
"sectors": sectors,
"sector_rotation": _rotation(sectors, previous_sectors),
}
def _rows(
inputs: dict[str, ProviderResult | dict[str, Any]], key: str
) -> tuple[dict[str, Any], ...]:
value = inputs.get(key)
return value.rows if isinstance(value, ProviderResult) else ()
def _pool(rows: tuple[dict[str, Any], ...], status: str) -> list[dict[str, Any]]:
result = []
for row in rows:
identifier = str(row.get("ts_code") or "")
amount = _number(row.get("amount")) * 1000
result.append(
{
"identifier": identifier,
"code": identifier.split(".")[0],
"name": str(row.get("name") or "").strip(),
"price": _number(row.get("close")),
"change": _number(row.get("pct_chg")),
"sector": str(row.get("industry") or "").strip(),
"reason": "",
"first_time": _time(row.get("first_time")),
"last_time": _time(row.get("last_time")),
"open_times": int(_number(row.get("open_times"))),
"streak": max(1, int(_number(row.get("limit_times"), 1))),
"turnover_rate": _number(row.get("turnover_ratio")),
"amount": amount,
"seal_amount": _number(row.get("fd_amount")),
"float_market_value": _number(row.get("float_mv")),
"status": status,
}
)
return result
def _yesterday(
previous: list[dict[str, Any]],
daily: dict[str, dict[str, Any]],
current: list[dict[str, Any]],
broken: list[dict[str, Any]],
down: list[dict[str, Any]],
) -> list[dict[str, Any]]:
current_map = {row["identifier"]: row for row in current}
broken_codes = {row["identifier"] for row in broken}
down_codes = {row["identifier"] for row in down}
result = []
for prior in previous:
identifier = prior["identifier"]
quote = daily.get(identifier, {})
change = _number(quote.get("pct_chg"))
if identifier in current_map:
outcome = "晋级"
elif identifier in broken_codes:
outcome = "炸板"
elif identifier in down_codes:
outcome = "跌停"
elif change > 0:
outcome = "红盘"
else:
outcome = "断板"
result.append(
{
"identifier": identifier,
"code": prior["code"],
"name": prior["name"],
"prior_streak": prior["streak"],
"current_streak": current_map.get(identifier, {}).get("streak", 0),
"current_change": change,
"current_price": _number(quote.get("close")),
"sector": prior["sector"],
"reason": prior["reason"],
"outcome": outcome,
}
)
return result
def _performance(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
result = []
for level in sorted({int(row["prior_streak"]) for row in rows}, reverse=True):
group = [row for row in rows if int(row["prior_streak"]) == level]
advanced = sum(row["outcome"] == "晋级" for row in group)
positive = sum(_number(row["current_change"]) > 0 for row in group)
outcomes = {
outcome: sum(row["outcome"] == outcome for row in group)
for outcome in ("晋级", "红盘", "断板", "炸板", "跌停")
}
result.append(
{
"level": level,
"count": len(group),
"advanced": advanced,
"red": outcomes["红盘"],
"broken": outcomes["断板"],
"opened": outcomes["炸板"],
"limit_down": outcomes["跌停"],
"advance_rate": round(advanced / len(group) * 100, 1),
"positive_rate": round(positive / len(group) * 100, 1),
"average_change": round(mean(_number(row["current_change"]) for row in group), 2),
}
)
return result
def _ladders(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
levels = sorted({int(row["streak"]) for row in rows}, reverse=True)
return [
{
"level": level,
"label": "首板" if level == 1 else f"{level}板",
"count": sum(int(row["streak"]) == level for row in rows),
"stocks": sorted(
(row for row in rows if int(row["streak"]) == level),
key=lambda row: row.get("first_time") or "99:99",
),
}
for level in levels
]
def _sectors(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
names = sorted({str(row.get("sector") or "").strip() for row in rows} - {""})
result = []
for name in names:
stocks = [row for row in rows if str(row.get("sector") or "").strip() == name]
leader = max(stocks, key=lambda row: (int(row["streak"]), _number(row["amount"])))
max_streak = max(int(row["streak"]) for row in stocks)
count = len(stocks)
result.append(
{
"name": name,
"count": count,
"strength": min(100, 44 + count * 8 + max_streak * 5),
"amount": round(sum(_number(row["amount"]) for row in stocks), 2),
"leader": leader["name"],
"representative": leader["identifier"],
"change": round(mean(_number(row["change"]) for row in stocks), 2),
"max_streak": max_streak,
}
)
return sorted(
result,
key=lambda row: (int(row["count"]), int(row["max_streak"]), _number(row["amount"])),
reverse=True,
)[:20]
def _rotation(
current: list[dict[str, Any]], previous: list[dict[str, Any]]
) -> list[dict[str, Any]]:
previous_map = {str(row["name"]): row for row in previous}
result = []
for rank, sector in enumerate(current[:12], start=1):
previous_count = int(previous_map.get(str(sector["name"]), {}).get("count") or 0)
delta = int(sector["count"]) - previous_count
result.append(
{
**sector,
"rank": rank,
"previous_count": previous_count,
"delta": delta,
"trend": "升温" if delta > 0 else "降温" if delta < 0 else "持平",
}
)
return result
def _number(value: Any, default: float = 0.0) -> float:
try:
number = float(value)
return number if number == number else default
except (TypeError, ValueError):
return default
def _time(value: Any) -> str:
text = str(value or "").strip().replace(":", "")
if len(text) < 4 or not text[:4].isdigit():
return ""
return f"{text[:2]}:{text[2:4]}"