rebuild(runtime): govern market operations and job truth
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -89,6 +90,14 @@ class IfindProvider:
|
||||
) -> dict[str, ProviderResult | dict[str, Any]]:
|
||||
raise ProviderError("iFinD is not the post-close snapshot authority")
|
||||
|
||||
def realtime_market_inputs(
|
||||
self,
|
||||
trade_date: str,
|
||||
previous_trade_date: str,
|
||||
identifiers: tuple[str, ...],
|
||||
) -> dict[str, ProviderResult | dict[str, Any]]:
|
||||
raise ProviderError("iFinD不承担全市场盘中快照计算")
|
||||
|
||||
def sector_members(self, representative: str, trade_date: str) -> ProviderResult:
|
||||
raise ProviderError("iFinD is not the Shenwan constituent authority")
|
||||
|
||||
@@ -145,6 +154,76 @@ class IfindProvider:
|
||||
),
|
||||
)
|
||||
|
||||
def event_reasons(self, trade_date: str) -> ProviderResult:
|
||||
date = datetime.fromisoformat(_display(trade_date)).date()
|
||||
display = f"{date.year}年{date.month}月{date.day}日"
|
||||
queries = {
|
||||
"limit_up": (
|
||||
f"{display}涨停股票,股票代码、股票简称、涨停原因、"
|
||||
"首次涨停时间、最终涨停时间、开板次数"
|
||||
),
|
||||
"broken": (
|
||||
f"{display}曾涨停但收盘未涨停的股票,股票代码、股票简称、"
|
||||
"涨停原因、首次涨停时间、开板次数"
|
||||
),
|
||||
"limit_down": f"{display}跌停股票,股票代码、股票简称、跌停原因",
|
||||
}
|
||||
rows: list[dict[str, Any]] = []
|
||||
completed = 0
|
||||
for event_type, query in queries.items():
|
||||
try:
|
||||
payload = self._request(
|
||||
"smart_stock_picking",
|
||||
{"searchstring": query, "searchtype": "stock"},
|
||||
)
|
||||
except ProviderError:
|
||||
continue
|
||||
completed += 1
|
||||
for raw in _result(
|
||||
payload, "event", "not_applicable", SnapshotState.FINAL
|
||||
).rows:
|
||||
identifier = _event_identifier(raw)
|
||||
if not identifier:
|
||||
continue
|
||||
reason_tokens = (
|
||||
("跌停原因", "风险线索", "原因")
|
||||
if event_type == "limit_down"
|
||||
else ("涨停原因类别", "涨停原因", "触板逻辑", "原因")
|
||||
)
|
||||
rows.append(
|
||||
{
|
||||
"event_type": event_type,
|
||||
"identifier": identifier,
|
||||
"reason": str(_event_field(raw, reason_tokens) or "").strip(),
|
||||
"first_time": _event_time(
|
||||
_event_field(
|
||||
raw, ("首次涨停时间", "首次触板时间", "首次封板时间")
|
||||
)
|
||||
),
|
||||
"last_time": _event_time(
|
||||
_event_field(
|
||||
raw, ("最终涨停时间", "最后涨停时间", "最后封板时间")
|
||||
)
|
||||
),
|
||||
"open_times": _event_integer(
|
||||
_event_field(raw, ("开板次数", "打开涨停次数"))
|
||||
),
|
||||
}
|
||||
)
|
||||
return ProviderResult(
|
||||
tuple(rows),
|
||||
ObservationMetadata(
|
||||
source=self.source,
|
||||
observed_at=datetime.now(SHANGHAI),
|
||||
unit="event",
|
||||
adjustment="not_applicable",
|
||||
freshness_seconds=0,
|
||||
coverage=completed / len(queries),
|
||||
state=SnapshotState.FINAL,
|
||||
usage=DataUsage.CALCULATION,
|
||||
),
|
||||
)
|
||||
|
||||
def screener_inputs(self, trade_dates: tuple[str, ...]) -> dict[str, ProviderResult | None]:
|
||||
raise ProviderError("iFinD尚未批准用于盘后因子批量计算")
|
||||
|
||||
@@ -295,3 +374,40 @@ def _error_code(payload: dict[str, Any]) -> int:
|
||||
return int(payload.get("errorcode", payload.get("code", 0)) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return -1
|
||||
|
||||
|
||||
def _event_field(row: dict[str, Any], tokens: tuple[str, ...]) -> Any:
|
||||
for token in tokens:
|
||||
for key, value in row.items():
|
||||
if token in str(key) and value not in (None, ""):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _event_identifier(row: dict[str, Any]) -> str:
|
||||
value = _event_field(row, ("股票代码", "证券代码", "代码", "thscode"))
|
||||
text = str(value or "").strip().upper()
|
||||
match = re.search(r"(?<!\d)(\d{6})(?!\d)", text)
|
||||
if not match:
|
||||
return ""
|
||||
code = match.group(1)
|
||||
if re.fullmatch(r"\d{6}\.(?:SH|SZ|BJ)", text):
|
||||
return text
|
||||
suffix = "BJ" if code.startswith(("4", "8", "9")) else "SH" if code.startswith("6") else "SZ"
|
||||
return f"{code}.{suffix}"
|
||||
|
||||
|
||||
def _event_time(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
match = re.search(r"(?<!\d)(\d{1,2}):(\d{2})(?::\d{2})?(?!\d)", text)
|
||||
if match:
|
||||
return f"{int(match.group(1)):02d}:{match.group(2)}"
|
||||
compact = re.search(r"(?<!\d)(\d{2})(\d{2})(\d{2})(?!\d)", text)
|
||||
return f"{compact.group(1)}:{compact.group(2)}" if compact else ""
|
||||
|
||||
|
||||
def _event_integer(value: Any) -> int | None:
|
||||
try:
|
||||
return max(0, int(float(value))) if value not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user