相关快讯:财联社电报点名条目进候选卡与实时计划,只当上下文不进判决(台账 050)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-08 14:07:05 +08:00
parent b69b57ff00
commit 69b25b629a
4 changed files with 136 additions and 1 deletions

15
card.py
View File

@ -456,5 +456,20 @@ def events_short(ev) -> str:
return f"{e['date'][5:]} {''.join(e.get('types') or [])}" + ("(复合)" if e.get("compound") else "") return f"{e['date'][5:]} {''.join(e.get('types') or [])}" + ("(复合)" if e.get("compound") else "")
def news_view(nv) -> str:
"""相关快讯的整句2026-09-08 台账 050财联社电报点名这只票的条目最新在前最多五条。只是上下文不判利好利空。"""
if not isinstance(nv, dict) or not nv.get("items"):
return "相关快讯:近 3 天财联社电报没有点名这只票"
parts = [f"{it.get('time')} [{it.get('level')}] {it.get('title')}" for it in nv["items"]]
return f"相关快讯(近 3 天 {nv.get('count')} 条,财联社电报点名,不判利好利空):" + "".join(parts)
def news_short(nv) -> str:
if not isinstance(nv, dict) or not nv.get("items"):
return ""
it = nv["items"][0]
return f"{it.get('time')} {str(it.get('title') or '')[:24]}"
def _pct(v) -> str: def _pct(v) -> str:
return "" if v is None else f"{v:+.1%}" return "" if v is None else f"{v:+.1%}"

View File

@ -302,6 +302,9 @@ def _assemble_cards(ds: str, codes: list, ev: dict, upside: pd.Series,
# 事件日字段从前复权行情表算,归成四情形。只展示加复盘分组,不进判决;任一读不到整体缺席不断产。 # 事件日字段从前复权行情表算,归成四情形。只展示加复盘分组,不进判决;任一读不到整体缺席不断产。
events = sources.analyst_events(codes, ds) events = sources.analyst_events(codes, ds)
ev_fields = sources.event_day_fields(codes, ds, events) ev_fields = sources.event_day_fields(codes, ds, events)
# 相关快讯2026-09-08 台账 050财联社电报近 3 天点名这只票的条目,只当上下文送人看与送研判,
# 不判正负、不进判决、不进定价状态。表读不到整体缺席不断产。
news = sources.news_flashes(codes, ds)
if risk is None: # collect 会传入读过一次的名单;单独调用时自己读 if risk is None: # collect 会传入读过一次的名单;单独调用时自己读
try: try:
risk = factors._risk_set() or set() # noqa: SLF001 —— 同仓自用 risk = factors._risk_set() or set() # noqa: SLF001 —— 同仓自用
@ -338,6 +341,7 @@ def _assemble_cards(ds: str, codes: list, ev: dict, upside: pd.Series,
"valuation": valuation.get(k), "valuation_text": card.valuation_view(valuation.get(k)), "valuation": valuation.get(k), "valuation_text": card.valuation_view(valuation.get(k)),
"events": events.get(k), "events_text": card.events_view(events.get(k)), "events": events.get(k), "events_text": card.events_view(events.get(k)),
"pricing_state": card.pricing_state(ev_fields.get(k)), "pricing_state": card.pricing_state(ev_fields.get(k)),
"news": news.get(k), "news_text": card.news_view(news.get(k)),
"theme": theme, "n_sources": n_sources, "chain_fit": evd["chain_fit"], "theme": theme, "n_sources": n_sources, "chain_fit": evd["chain_fit"],
"started_source": "moved_view" if mv else None, "started_source": "moved_view" if mv else None,
"logic_claims": evd["logic"], "logic_claims": evd["logic"],
@ -470,6 +474,8 @@ def collect(date: str | None = None, top: int = 20, obs_top: int = 10,
events=c.get("events"), events_text=c.get("events_text"), events=c.get("events"), events_text=c.get("events_text"),
pricing_state=c.get("pricing_state"), pricing_state=c.get("pricing_state"),
pricing_text=card.pricing_view(c.get("pricing_state")), pricing_text=card.pricing_view(c.get("pricing_state")),
# 相关快讯2026-09-08 台账 050原值给程序整句给人只是上下文不进判决。
news=c.get("news"), news_text=c.get("news_text"),
card={"pct0": c.get("pct0"), "net_z": c.get("net_z"), card={"pct0": c.get("pct0"), "net_z": c.get("net_z"),
"heat_chg": c.get("heat_chg"), "accum": c.get("accum"), "heat_chg": c.get("heat_chg"), "accum": c.get("accum"),
"night": c.get("night"), "gates": c.get("gates"), "night": c.get("night"), "gates": c.get("gates"),

View File

@ -30,6 +30,8 @@ from __future__ import annotations
import datetime as dt import datetime as dt
import json import json
import re
import logging
import statistics import statistics
from collections import defaultdict from collections import defaultdict
@ -1031,3 +1033,73 @@ def event_day_fields(codes, ds: str, events: dict, *, hist=None, read_mysql=None
def _r4(v): def _r4(v):
return None if v is None else round(float(v), 4) return None if v is None else round(float(v), 4)
# ---------------------------------------------------------------------------
# 相关快讯2026-09-08台账 050财联社电报表 gp_cls_telegraph153 库,与行情表同一连接)里
# 用 stock_list_json 点名了这只票的条目。只当上下文,不当"正向事件"——电报说的是发生了什么,
# 不说利好利空,正负由看卡的人与择时决策系统的研判去判;不进判决、不进定价状态、不进复盘分组。
# 窗口按自然日往前数、不封顶到数据日07:10 出计划时,昨晚与今晨的快讯都要在。
# ---------------------------------------------------------------------------
NEWS_WINDOW_DAYS = 3
NEWS_MAX_PER_CODE = 5
NEWS_TITLE_LEN = 60
_news_log = logging.getLogger("bridge.news")
def _norm_stock_id(sid) -> str | None:
"""财联社的 StockIDsh688293 / sz000998→ 前缀码SH688293。认不出返回 None。"""
s = str(sid or "").strip().upper()
if len(s) == 8 and s[:2] in ("SH", "SZ", "BJ") and s[2:].isdigit():
return s
return None
def _news_title(title, content) -> str:
t = str(title or content or "").strip()
t = re.sub(r"^财联社\d{1,2}月\d{1,2}日电[,]?", "", t).strip()
return t[:NEWS_TITLE_LEN] + ("" if len(t) > NEWS_TITLE_LEN else "")
def news_flashes(codes, ds: str, *, days: int = NEWS_WINDOW_DAYS, read_mysql=None,
limit_per_code: int = NEWS_MAX_PER_CODE) -> dict:
"""每只票近 days 个自然日被财联社电报点名的快讯,按前缀码索引;没被点名的票不在字典里。
返回 {k: {"count": 条数, "latest": "MM-DD HH:MM", "items": [{time, level, title, url}, ...] 最新在前}}
level 是财联社自己的重要度A 红色B 加粗C 普通原样带出不解释表读不到返回空字典不断产"""
rm = read_mysql or db.read_mysql
want = {c for c in (codes or []) if c}
if not want:
return {}
try:
since = (dt.date.fromisoformat(ds) - dt.timedelta(days=int(days))).isoformat()
except ValueError:
return {}
try:
df = rm("factor",
"SELECT cls_id, level, publish_time, title, content, stock_list_json, article_url "
"FROM gp_cls_telegraph WHERE publish_time >= %s AND JSON_LENGTH(stock_list_json) > 0 "
"ORDER BY publish_time DESC", (since,))
except Exception as e: # noqa: BLE001 —— 表没建、连接失败:整体缺席
_news_log.warning("news_flashes 读不到 gp_cls_telegraph: %r", e)
return {}
out: dict = {}
for r in df.to_dict("records"):
raw = r.get("stock_list_json")
try:
lst = json.loads(raw) if isinstance(raw, (str, bytes)) else (raw or [])
except (TypeError, ValueError):
continue
ks = {k for k in (_norm_stock_id((it or {}).get("StockID")) for it in lst) if k and k in want}
if not ks:
continue
ts = r.get("publish_time")
when = ts.strftime("%m-%d %H:%M") if hasattr(ts, "strftime") else str(ts or "")[5:16]
item = {"time": when, "level": str(r.get("level") or "C"), "cls_id": r.get("cls_id"),
"title": _news_title(r.get("title"), r.get("content")), "url": r.get("article_url")}
for k in ks:
slot = out.setdefault(k, {"count": 0, "latest": when, "items": []})
slot["count"] += 1
if len(slot["items"]) < limit_per_code:
slot["items"].append(item)
return out

View File

@ -189,11 +189,53 @@ def test_pricing_state():
t("没有事件的整句", "没有券商正向事件" in card.events_view(None)) t("没有事件的整句", "没有券商正向事件" in card.events_view(None))
class _DF:
def __init__(self, rows): self._rows = rows
def to_dict(self, _kind): return self._rows
def test_news():
"""相关快讯(台账 050按财联社 StockID 标签落到前缀码,最新在前、最多五条、只当上下文。"""
import datetime as dt
import json
rows = [
{"cls_id": 3, "level": "C", "publish_time": dt.datetime(2026, 9, 8, 12, 39), "article_url": "u3",
"title": "燕东微上半年8英寸SiN产线产能达3000片/月", "content": "",
"stock_list_json": json.dumps([{"StockID": "sh688172", "name": "燕东微"}])},
{"cls_id": 2, "level": "B", "publish_time": dt.datetime(2026, 9, 8, 11, 32), "article_url": "u2",
"title": "财联社9月8日电农业板块再度拉升 金健米业17天9板", "content": "",
"stock_list_json": json.dumps([{"StockID": "sz000998"}, {"StockID": "sh688172"}, {"StockID": "bad"}])},
{"cls_id": 1, "level": "C", "publish_time": dt.datetime(2026, 9, 7, 9, 0), "article_url": "u1",
"title": None, "content": "财联社9月7日电华鲁恒升尿素价格未来走势存在较大不确定性",
"stock_list_json": "not json"},
]
calls = []
def rm(which, sql, params=None):
calls.append((which, params)); return _DF(rows)
out = sources.news_flashes(["SH688172", "SZ000998", "SH600000"], "2026-09-08", read_mysql=rm)
t("用行情同一连接、窗口按自然日往前 3 天", calls[0][0] == "factor" and calls[0][1] == ("2026-09-05",))
t("燕东微两条、最新在前", out["SH688172"]["count"] == 2 and out["SH688172"]["items"][0]["cls_id"] == 3)
t("标题去掉「财联社X月X日电」前缀", out["SZ000998"]["items"][0]["title"].startswith("农业板块"))
t("没被点名的票不在字典里、坏 JSON 跳过", "SH600000" not in out and all(i["cls_id"] != 1 for i in out["SH688172"]["items"]))
t("时间落成 MM-DD HH:MM", out["SH688172"]["latest"] == "09-08 12:39")
big = [dict(rows[0], cls_id=10 + i, publish_time=dt.datetime(2026, 9, 8, 13, i)) for i in range(7)]
out2 = sources.news_flashes(["SH688172"], "2026-09-08", read_mysql=lambda *a, **k: _DF(big))
t("最多五条但计数照实", len(out2["SH688172"]["items"]) == 5 and out2["SH688172"]["count"] == 7)
def boom(*a, **k): raise RuntimeError("no table")
t("表读不到整体缺席不断产", sources.news_flashes(["SH688172"], "2026-09-08", read_mysql=boom) == {})
t("日期坏了也不断产", sources.news_flashes(["SH688172"], "bad", read_mysql=rm) == {})
v = card.news_view(out["SH688172"])
t("整句带条数与不判正负的说明", v.startswith("相关快讯(近 3 天 2 条") and "不判利好利空" in v and "[C]" in v)
t("没有快讯的整句", "没有点名" in card.news_view(None) and card.news_short(None) == "")
t("短句是最新一条", card.news_short(out["SH688172"]).startswith("09-08 12:39 燕东微"))
def main(): def main():
test_events() test_events()
test_event_day_fields() test_event_day_fields()
test_pricing_state() test_pricing_state()
print("ALL OK — 四类事件 / 事件日字段 / 定价状态四情形 / 卡上文字 全部通过") test_news()
print("ALL OK — 四类事件 / 事件日字段 / 定价状态四情形 / 卡上文字 / 相关快讯 全部通过")
if __name__ == "__main__": if __name__ == "__main__":