161 lines
6.7 KiB
Python
161 lines
6.7 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, _prices_equal
|
|
|
|
|
|
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 row.get("is_open") == 1 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 result
|
|
|
|
@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(_number(row.get("fd_amount")) / 10000, 0),
|
|
"float_mv_billion": round(_number(row.get("float_mv")) / 100000000, 1),
|
|
"status": status,
|
|
}
|