主网站只向中枢要业务数据;来源选择、切源、补数全部在中枢内部完成,失败不再走东财/腾讯/Tushare 保底。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
233 lines
8.9 KiB
Python
233 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from backend.data.numbers import finite_number as _number
|
|
from backend.data.providers.tushare_helpers import (
|
|
_display_time,
|
|
_optional_number,
|
|
_prices_equal,
|
|
calendar_is_open,
|
|
)
|
|
|
|
|
|
class DailyMarketMixin:
|
|
def resolve_trade_context(self, requested: str) -> tuple[str, str]:
|
|
requested_rows = self.query(
|
|
"trade_cal",
|
|
{"exchange": "SSE", "start_date": requested, "end_date": requested},
|
|
"cal_date,is_open,pretrade_date",
|
|
)
|
|
if not requested_rows:
|
|
trade_date = requested
|
|
else:
|
|
row = requested_rows[0]
|
|
trade_date = (
|
|
row["cal_date"]
|
|
if calendar_is_open(row.get("is_open"))
|
|
else row.get("pretrade_date", requested)
|
|
)
|
|
|
|
resolved_rows = self.query(
|
|
"trade_cal",
|
|
{"exchange": "SSE", "start_date": trade_date, "end_date": trade_date},
|
|
"cal_date,is_open,pretrade_date",
|
|
)
|
|
previous = resolved_rows[0].get("pretrade_date") if resolved_rows else ""
|
|
return trade_date, previous or trade_date
|
|
|
|
def _load_daily(self, trade_date: str) -> list[dict[str, Any]]:
|
|
return self.query(
|
|
"daily",
|
|
{"trade_date": trade_date},
|
|
"ts_code,trade_date,open,high,low,close,pct_chg,amount",
|
|
)
|
|
|
|
def _load_limit_type(self, trade_date: str, limit_type: str) -> list[dict[str, Any]]:
|
|
fields = (
|
|
"trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
|
|
"float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
|
|
"open_times,up_stat,limit_times"
|
|
)
|
|
rows = self.query(
|
|
"limit_list_d",
|
|
{"trade_date": trade_date, "limit_type": limit_type},
|
|
fields,
|
|
)
|
|
for row in rows:
|
|
row["limit_type"] = limit_type
|
|
row["amount_unit"] = "yuan"
|
|
return rows
|
|
|
|
def _load_limit_lists(self, trade_date: str) -> list[dict[str, Any]]:
|
|
rows: list[dict[str, Any]] = []
|
|
for limit_type in ("U", "D", "Z"):
|
|
rows.extend(self._load_limit_type(trade_date, limit_type))
|
|
return rows
|
|
|
|
def _derive_limits(
|
|
self,
|
|
trade_date: str,
|
|
daily: list[dict[str, Any]],
|
|
price_limits: list[dict[str, Any]] | None = None,
|
|
basic_rows: list[dict[str, Any]] | None = None,
|
|
previous_limit_rows: list[dict[str, Any]] | None = None,
|
|
capital_rows: list[dict[str, Any]] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
if price_limits is None:
|
|
price_limits = self.query(
|
|
"stk_limit",
|
|
{"trade_date": trade_date},
|
|
"ts_code,trade_date,up_limit,down_limit",
|
|
)
|
|
limit_map = {row["ts_code"]: row for row in price_limits}
|
|
if basic_rows is None:
|
|
basic_rows = self.query(
|
|
"stock_basic",
|
|
{"list_status": "L"},
|
|
"ts_code,name,industry",
|
|
)
|
|
basic_map = {row["ts_code"]: row for row in basic_rows}
|
|
previous_limit_map = {
|
|
str(row.get("ts_code") or ""): row for row in (previous_limit_rows or [])
|
|
}
|
|
capital_map = {
|
|
str(row.get("ts_code") or ""): row for row in (capital_rows or [])
|
|
}
|
|
|
|
result: list[dict[str, Any]] = []
|
|
for row in daily:
|
|
bounds = limit_map.get(row.get("ts_code"))
|
|
if not bounds or row.get("close") is None:
|
|
continue
|
|
limit_type = ""
|
|
if _prices_equal(row["close"], bounds.get("up_limit")):
|
|
limit_type = "U"
|
|
elif _prices_equal(row["close"], bounds.get("down_limit")):
|
|
limit_type = "D"
|
|
elif _prices_equal(row.get("high"), bounds.get("up_limit")):
|
|
limit_type = "Z"
|
|
if not limit_type:
|
|
continue
|
|
basic = basic_map.get(row["ts_code"], {})
|
|
previous_limit = previous_limit_map.get(str(row.get("ts_code") or ""), {})
|
|
streak = (
|
|
max(1, int(_number(previous_limit.get("limit_times"), 1)) + 1)
|
|
if limit_type == "U" and previous_limit
|
|
else 1
|
|
)
|
|
item = {
|
|
**row,
|
|
"name": basic.get("name", "--"),
|
|
"industry": basic.get("industry") or "其他",
|
|
"limit_type": limit_type,
|
|
"limit_times": streak,
|
|
"open_times": 1 if limit_type == "Z" else 0,
|
|
"amount_unit": row.get("amount_unit") or "thousand_yuan",
|
|
}
|
|
if row.get("amount_unit") == "yuan":
|
|
capital = capital_map.get(str(row.get("ts_code") or ""), {})
|
|
if not capital and capital_rows is None:
|
|
capital = self._latest_capital(str(row.get("ts_code") or ""), trade_date)
|
|
float_share = _number(capital.get("float_share"))
|
|
item["turnover_ratio"] = (
|
|
_number(row.get("vol")) / float_share / 100 if float_share else 0
|
|
)
|
|
item["turnover_source"] = (
|
|
"rt_volume/latest_float_share" if float_share else "unavailable"
|
|
)
|
|
item["capital_trade_date"] = str(capital.get("trade_date") or "")
|
|
result.append(item)
|
|
return self._overlay_board_fields(result, trade_date)
|
|
|
|
def _overlay_board_fields(
|
|
self,
|
|
rows: list[dict[str, Any]],
|
|
trade_date: str,
|
|
) -> list[dict[str, Any]]:
|
|
if not rows:
|
|
return rows
|
|
official = self._official_board_map(trade_date)
|
|
free = self._free_board_map(trade_date) if not official else {}
|
|
merged: list[dict[str, Any]] = []
|
|
for row in rows:
|
|
code = str(row.get("ts_code") or "")
|
|
extra = official.get(code) or free.get(code) or {}
|
|
if not extra:
|
|
merged.append(row)
|
|
continue
|
|
item = dict(row)
|
|
for key in (
|
|
"first_time",
|
|
"last_time",
|
|
"fd_amount",
|
|
"open_times",
|
|
"limit_times",
|
|
"turnover_ratio",
|
|
):
|
|
incoming = extra.get(key)
|
|
current = item.get(key)
|
|
if incoming in (None, "", "--"):
|
|
continue
|
|
if current in (None, "", "--", 0, 0.0):
|
|
item[key] = incoming
|
|
merged.append(item)
|
|
return merged
|
|
|
|
def _official_board_map(self, trade_date: str) -> dict[str, dict[str, Any]]:
|
|
mapped: dict[str, dict[str, Any]] = {}
|
|
try:
|
|
for row in self._load_limit_lists(trade_date):
|
|
code = str(row.get("ts_code") or "")
|
|
if code:
|
|
mapped[code] = row
|
|
except Exception:
|
|
return {}
|
|
return mapped
|
|
|
|
def _free_board_map(self, trade_date: str) -> dict[str, dict[str, Any]]:
|
|
loader = getattr(self, "try_limit_pool", None)
|
|
if not callable(loader):
|
|
return {}
|
|
try:
|
|
rows = loader(trade_date) or []
|
|
except Exception:
|
|
return {}
|
|
return {
|
|
str(row.get("ts_code") or ""): row
|
|
for row in rows
|
|
if row.get("ts_code")
|
|
}
|
|
|
|
@staticmethod
|
|
def _normalize_limit(row: dict[str, Any], status: str) -> dict[str, Any]:
|
|
amount = _number(row.get("amount"))
|
|
if row.get("amount_unit") == "thousand_yuan":
|
|
amount_billion = amount / 100000
|
|
else:
|
|
amount_billion = amount / 100000000
|
|
return {
|
|
"code": str(row.get("ts_code", "")).split(".")[0],
|
|
"ts_code": row.get("ts_code", ""),
|
|
"name": row.get("name") or "--",
|
|
"price": _number(row.get("close")),
|
|
"change": _number(row.get("pct_chg")),
|
|
"sector": row.get("industry") or "其他",
|
|
"reason": row.get("industry") or "待补充",
|
|
"first_time": _display_time(row.get("first_time")),
|
|
"last_time": _display_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")),
|
|
"turnover_source": row.get("turnover_source") or "provider",
|
|
"capital_trade_date": row.get("capital_trade_date") or "",
|
|
"amount_billion": round(amount_billion, 2),
|
|
"seal_amount_million": (
|
|
round(fd / 10000, 0)
|
|
if (fd := _optional_number(row.get("fd_amount"))) is not None
|
|
else None
|
|
),
|
|
"float_mv_billion": round(_number(row.get("float_mv")) / 100000000, 1),
|
|
"status": status,
|
|
}
|