rebuild(stage-6): deliver emotion and market pools
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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"), "跌停")
|
||||
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(
|
||||
_pool(_rows(inputs, "previous_limit_up"), "涨停"),
|
||||
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
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
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 _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]}"
|
||||
Reference in New Issue
Block a user