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

311 lines
11 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(_amount_yuan(row) 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 build_realtime_inputs(
inputs: dict[str, ProviderResult | dict[str, Any]],
directory: dict[str, dict[str, Any]],
) -> dict[str, ProviderResult | dict[str, Any]]:
daily_result = inputs.get("daily")
limits_result = inputs.get("price_limits")
previous_result = inputs.get("previous_limit_up")
if not isinstance(daily_result, ProviderResult) or not isinstance(
limits_result, ProviderResult
):
return inputs
prices = {str(row.get("ts_code") or ""): row for row in limits_result.rows}
previous = {
str(row.get("ts_code") or ""): row
for row in previous_result.rows
} if isinstance(previous_result, ProviderResult) else {}
pools: dict[str, list[dict[str, Any]]] = {
"limit_up": [],
"limit_down": [],
"broken": [],
}
for quote in daily_result.rows:
identifier = str(quote.get("ts_code") or "")
price = prices.get(identifier) or {}
current = _number(quote.get("close"))
high = _number(quote.get("high"))
up_limit = _number(price.get("up_limit"))
down_limit = _number(price.get("down_limit"))
event_type = ""
if up_limit > 0 and current >= up_limit - 0.001:
event_type = "limit_up"
elif up_limit > 0 and high >= up_limit - 0.001:
event_type = "broken"
elif down_limit > 0 and current <= down_limit + 0.001:
event_type = "limit_down"
if not event_type:
continue
identity = directory.get(identifier) or {}
prior_streak = int(_number(previous.get(identifier, {}).get("limit_times")))
pools[event_type].append(
{
**quote,
"name": str(quote.get("name") or identity.get("name") or ""),
"industry": str(identity.get("sector") or ""),
"first_time": "",
"last_time": "",
"open_times": 0,
"limit_times": prior_streak + 1 if event_type == "limit_up" else 1,
"fd_amount": 0,
}
)
metadata = daily_result.metadata
return {
**inputs,
"limit_up": ProviderResult(tuple(pools["limit_up"]), metadata),
"limit_down": ProviderResult(tuple(pools["limit_down"]), metadata),
"broken": ProviderResult(tuple(pools["broken"]), metadata),
}
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 = _amount_yuan(row)
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 _amount_yuan(row: dict[str, Any]) -> float:
amount = _number(row.get("amount"))
return amount if row.get("amount_unit") == "yuan" else amount * 1000
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]}"