Files
xiaobai-review/backend/data/datahub/native.py
T
605f97e5df feat(HEL-463): 接入剩余行情数据到 datahub
扩展盘后正式集(涨跌停/人气/龙虎榜/板块日线)与盘中观察 API(报价/指数/分时),网站 bridge 按开关接入并回退旧链路;问天改为按数据依赖跟随开关,不再整栈强制旧路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-05 17:30:58 +08:00

205 lines
7.1 KiB
Python

from __future__ import annotations
from typing import Any
from backend.data.numbers import finite_number
AMOUNT_THOUSAND_YUAN = 1000.0
AMOUNT_WAN_YUAN = 10000.0
VOLUME_LOT = 100.0
API_TO_DATASET = {
"trade_cal": "calendar",
"stock_basic": "stocks",
"daily": "daily",
"daily_basic": "valuation",
"index_daily": "index_daily",
"moneyflow": "moneyflow",
"stk_auction": "auction",
"limit_list_d": "limit_events",
"ths_hot": "popularity",
"dc_hot": "popularity",
"hm_detail": "dragon_tiger",
"ths_daily": "sector_daily",
"dc_index": "sector_daily",
"sw_daily": "sector_daily",
}
SCALE_FIELDS = {
"daily": {"vol": VOLUME_LOT, "amount": AMOUNT_THOUSAND_YUAN},
"index_daily": {"vol": VOLUME_LOT, "amount": AMOUNT_THOUSAND_YUAN},
"valuation": {"total_mv": AMOUNT_WAN_YUAN, "circ_mv": AMOUNT_WAN_YUAN},
"moneyflow": {
"buy_sm_amount": AMOUNT_WAN_YUAN,
"sell_sm_amount": AMOUNT_WAN_YUAN,
"buy_md_amount": AMOUNT_WAN_YUAN,
"sell_md_amount": AMOUNT_WAN_YUAN,
"buy_lg_amount": AMOUNT_WAN_YUAN,
"sell_lg_amount": AMOUNT_WAN_YUAN,
"buy_elg_amount": AMOUNT_WAN_YUAN,
"sell_elg_amount": AMOUNT_WAN_YUAN,
"net_mf_amount": AMOUNT_WAN_YUAN,
},
"auction": {"vol": VOLUME_LOT, "float_share": AMOUNT_WAN_YUAN},
"limit_events": {
"limit_amount": AMOUNT_WAN_YUAN,
"float_mv": AMOUNT_WAN_YUAN,
"total_mv": AMOUNT_WAN_YUAN,
},
"dragon_tiger": {
"buy_amount": AMOUNT_WAN_YUAN,
"sell_amount": AMOUNT_WAN_YUAN,
"net_amount": AMOUNT_WAN_YUAN,
},
}
def yyyymmdd(value: Any) -> str:
return str(value or "").replace("-", "")[:8]
def to_native_rows(dataset: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [to_native_row(dataset, row) for row in rows]
def to_native_row(dataset: str, row: dict[str, Any]) -> dict[str, Any]:
if dataset == "calendar":
is_open = row.get("is_open")
return {
"exchange": str(row.get("exchange") or "SSE"),
"cal_date": yyyymmdd(row.get("cal_date")),
"is_open": 1 if is_open in (True, 1, "1", "Y", "y") else 0,
"pretrade_date": yyyymmdd(row.get("pretrade_date")) or None,
}
converted = dict(row)
converted.pop("batch_id", None)
if "volume" in converted and "vol" not in converted:
converted["vol"] = converted.pop("volume")
elif "volume" in converted:
converted.pop("volume", None)
scales = SCALE_FIELDS.get(dataset) or {}
for field, factor in scales.items():
if field in converted:
converted[field] = _unscale(converted.get(field), factor)
if dataset == "stocks":
converted.pop("updated_at", None)
if dataset == "popularity":
# keep hub source; callers filter ths/dc themselves when needed
if converted.get("ts_name") and not converted.get("name"):
converted["name"] = converted.get("ts_name")
if dataset == "dragon_tiger":
if converted.get("ts_name") and not converted.get("name"):
converted["name"] = converted.get("ts_name")
if dataset == "sector_daily":
if converted.get("pct_change") is not None and converted.get("pct_chg") is None:
converted["pct_chg"] = converted.get("pct_change")
return converted
def to_canonical_row(dataset: str, row: dict[str, Any]) -> dict[str, Any]:
if dataset == "calendar":
is_open = row.get("is_open")
return {
"exchange": str(row.get("exchange") or "SSE"),
"cal_date": yyyymmdd(row.get("cal_date")),
"is_open": 1 if is_open in (True, 1, "1", "Y", "y") else 0,
"pretrade_date": yyyymmdd(row.get("pretrade_date")) or None,
}
converted = dict(row)
if "volume" in converted and "vol" not in converted:
converted["vol"] = converted.pop("volume")
scales = SCALE_FIELDS.get(dataset) or {}
for field, factor in scales.items():
if field in converted:
converted[field] = _scale(converted.get(field), factor)
return converted
def row_key(dataset: str, row: dict[str, Any]) -> tuple[str, ...]:
if dataset == "calendar":
return (yyyymmdd(row.get("cal_date")),)
if dataset == "stocks":
return (str(row.get("ts_code") or "").upper(),)
if dataset == "status":
return (str(row.get("dataset") or ""), yyyymmdd(row.get("trade_date")))
if dataset == "limit_events":
return (
str(row.get("ts_code") or "").upper(),
yyyymmdd(row.get("trade_date")),
str(row.get("limit_type") or ""),
)
if dataset == "popularity":
return (
str(row.get("ts_code") or "").upper(),
yyyymmdd(row.get("trade_date")),
str(row.get("source") or ""),
)
if dataset == "dragon_tiger":
return (
str(row.get("ts_code") or "").upper(),
yyyymmdd(row.get("trade_date")),
str(row.get("hm_name") or ""),
)
if dataset == "sector_daily":
return (
str(row.get("ts_code") or "").upper(),
yyyymmdd(row.get("trade_date")),
str(row.get("family") or ""),
)
return (str(row.get("ts_code") or "").upper(), yyyymmdd(row.get("trade_date")))
def project_fields(rows: list[dict[str, Any]], fields: str) -> list[dict[str, Any]]:
keys = [item.strip() for item in str(fields or "").split(",") if item.strip()]
if not keys:
return rows
return [{key: row.get(key) for key in keys} for row in rows]
def filter_stock_rows(rows: list[dict[str, Any]], params: dict[str, Any] | None) -> list[dict[str, Any]]:
payload = params or {}
ts_code = str(payload.get("ts_code") or "").strip().upper()
status = str(payload.get("list_status") or "").strip()
name = str(payload.get("name") or "").strip()
filtered = rows
if ts_code:
filtered = [row for row in filtered if str(row.get("ts_code") or "").upper() == ts_code]
if status:
filtered = [row for row in filtered if str(row.get("list_status") or status) == status]
if name:
filtered = [row for row in filtered if name.casefold() in str(row.get("name") or "").casefold()]
return filtered
def filter_calendar_rows(rows: list[dict[str, Any]], params: dict[str, Any] | None) -> list[dict[str, Any]]:
payload = params or {}
if payload.get("is_open") in (1, "1", True):
return [row for row in rows if int(row.get("is_open") or 0) == 1]
if payload.get("is_open") in (0, "0", False):
return [row for row in rows if int(row.get("is_open") or 0) == 0]
return rows
def _scale(value: Any, factor: float) -> float | None:
number = _optional_number(value)
if number is None:
return None
return number * factor
def _unscale(value: Any, factor: float) -> float | None:
number = _optional_number(value)
if number is None or factor == 0:
return None
return number / factor
def _optional_number(value: Any) -> float | None:
if value in (None, ""):
return None
number = finite_number(value, default=float("nan"))
if number != number:
return None
return number