原六个加脑机接口、氢能、深海科技、固态电池;太空光伏收进商业航空航天
This commit is contained in:
parent
f5d5232d7c
commit
235731855b
|
|
@ -114,3 +114,8 @@ UPSTREAM_MEMBER_CAP = int(os.environ.get("UPSTREAM_MEMBER_CAP", "600"))
|
||||||
# 再交赛道成员,观察档的产业链锚也并入赛道成员。
|
# 再交赛道成员,观察档的产业链锚也并入赛道成员。
|
||||||
TRACKS_YML = os.environ.get("TRACKS_YML", "config/frontier_tracks.yml")
|
TRACKS_YML = os.environ.get("TRACKS_YML", "config/frontier_tracks.yml")
|
||||||
ENABLE_TRACK_GATE = os.environ.get("ENABLE_TRACK_GATE", "0") == "1"
|
ENABLE_TRACK_GATE = os.environ.get("ENABLE_TRACK_GATE", "0") == "1"
|
||||||
|
|
||||||
|
# --- 重大风险股闸(07-31 拍板:默认杜绝)-------------------------------------
|
||||||
|
# ST / *ST / S(S)T / 退市族按证券简称识别,默认挡在档位之外——gate 记 0(不采纳),
|
||||||
|
# 因此不进主榜、观察档、计划与升降档。研究口径想看全貌时置 0 关闭。
|
||||||
|
EXCLUDE_RISK_NAMES = os.environ.get("EXCLUDE_RISK_NAMES", "1") == "1"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
# 十五五前沿赛道 · 产业链映射(硬门槛 C 的唯一事实源)
|
# 十五五前沿赛道 · 产业链映射(硬门槛 C 的唯一事实源)
|
||||||
# 规格出处:《量化因子导出与合成设计》v2.0 §3.2(tracks.py + config/frontier_tracks.yml)
|
# 规格出处:《量化因子导出与合成设计》v2.0 §3.2(tracks.py + config/frontier_tracks.yml)
|
||||||
#
|
#
|
||||||
# 版本:v0.2 草案(2026-07-30 晚:按环节投影首跑的链名清单补图谱双轨;待逐条 review 转正)
|
# 版本:v1.0 转正(2026-07-31 用户批:13 条拍板"全按建议"——太空光伏收进商业
|
||||||
|
# 航空航天,其余边界主题/链维持不收;四个候选赛道全部转正。§9-2 关闭,
|
||||||
|
# 赛道清单 = 十个 confirmed。行内"已拍"注释即拍板记录,勿删。)
|
||||||
# 映射源:industry_pools 07-29 快照(317 池)+ G1 覆盖初判(07-27)
|
# 映射源:industry_pools 07-29 快照(317 池)+ G1 覆盖初判(07-27)
|
||||||
# + segment_members_projection 首跑链名清单(07-30,用户实测回传)
|
# + segment_members_projection 首跑链名清单(07-30,用户实测回传)
|
||||||
# 行尾注释里的数字 = 主题成员数(池口径)或链/环节的上市成员数(投影口径),会随涌现增长
|
# 行尾注释里的数字 = 主题成员数(池口径)或链/环节的上市成员数(投影口径),会随涌现增长
|
||||||
|
|
@ -13,7 +15,7 @@
|
||||||
# kg_segments 环节名 → 图谱环节(强锚,graph_segment;环节层碎,平均 2.2 成员/组,
|
# kg_segments 环节名 → 图谱环节(强锚,graph_segment;环节层碎,平均 2.2 成员/组,
|
||||||
# 所以以链名为主、环节名点睛)
|
# 所以以链名为主、环节名点睛)
|
||||||
# kg_concepts 概念标签——等 claims 口径体检后再启用,暂全空。
|
# kg_concepts 概念标签——等 claims 口径体检后再启用,暂全空。
|
||||||
# status: confirmed = §3.2 已明确 | candidate = 覆盖非空、待 §9-2 定夺
|
# status: 全部 confirmed(07-31 起;曾有 candidate 一档,§9-2 关闭后不再使用)
|
||||||
# layer(材料→设备→制造→应用)按评审结论不阻塞 G2,本版不标。
|
# layer(材料→设备→制造→应用)按评审结论不阻塞 G2,本版不标。
|
||||||
# 数据卫生备忘:投影里有 578 条空串链名(抽取时未写链修饰的边)——映射侧已忽略。
|
# 数据卫生备忘:投影里有 578 条空串链名(抽取时未写链修饰的边)——映射侧已忽略。
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -47,10 +49,11 @@ tracks:
|
||||||
- 低轨卫星产业链 # 18
|
- 低轨卫星产业链 # 18
|
||||||
- 卫星导航 # 5
|
- 卫星导航 # 5
|
||||||
- 卫星终端 # 4
|
- 卫星终端 # 4
|
||||||
|
- 太空光伏 # 23【已拍 07-31:收——航天应用属性明确】
|
||||||
kg_chains: [商业航天, 卫星, 卫星互联网, 航天] # 36/31/17/14
|
kg_chains: [商业航天, 卫星, 卫星互联网, 航天] # 36/31/17/14
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): 太空光伏(23) 归航天还是能源?大飞机(11) 属航空非航天,本版未收。
|
# 已拍(07-31): 大飞机(11) 不收——民航制造,航天与低空都不贴。
|
||||||
|
|
||||||
- name: 通信
|
- name: 通信
|
||||||
status: confirmed
|
status: confirmed
|
||||||
|
|
@ -67,7 +70,7 @@ tracks:
|
||||||
kg_chains: [光通信, 通信, 光模块] # 68/38/15
|
kg_chains: [光通信, 通信, 光模块] # 68/38/15
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): 网络端(15 主题/14 链)、ICT(6) 名称含混,两侧均未收。
|
# 已拍(07-31): 网络端(15 主题/14 链)、ICT(6) 不收——名称含混。
|
||||||
|
|
||||||
- name: 算力
|
- name: 算力
|
||||||
status: confirmed
|
status: confirmed
|
||||||
|
|
@ -96,9 +99,10 @@ tracks:
|
||||||
kg_chains: [AIDC, AI 服务器, AI 基础设施, 先进封装] # 28/15/15/18
|
kg_chains: [AIDC, AI 服务器, AI 基础设施, 先进封装] # 28/15/15/18
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): PCB(67 主题/24 链) 偏泛收不收;散热材料(14)/热管理(14)/柴油发电机组(9,备电) 是否入链。
|
# 已拍(07-31): PCB(67 主题/24 链) 不收——AIPCB 已收够用;散热材料(14)/
|
||||||
# TODO(拍板): 泛半导体链(半导体117/集成电路32/封测25/半导体设备16)收不收——
|
# 热管理(14)/柴油发电机组(9) 不收——混消费电子与通用工业。
|
||||||
# 收了"算力"会宽成"半导体",且十五五清单里半导体不是独立赛道,先未收。
|
# 已拍(07-31): 泛半导体链(半导体117/集成电路32/封测25/半导体设备16)不收——
|
||||||
|
# 收了"算力"会宽成"半导体",且十五五清单里半导体不是独立赛道。
|
||||||
|
|
||||||
- name: 人工智能
|
- name: 人工智能
|
||||||
status: confirmed
|
status: confirmed
|
||||||
|
|
@ -116,8 +120,8 @@ tracks:
|
||||||
kg_chains: [人形机器人] # 78
|
kg_chains: [人形机器人] # 78
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): 机器人(212 主题/86 链) 含大量工业机器人存量,偏泛——两侧均未收。
|
# 已拍(07-31): 机器人(212 主题/86 链) 不收——工业机器人存量大,人形机器人已收。
|
||||||
# TODO(拍板): 与"算力"的边界:AI 基础设施类主题本版归算力,AI 应用类归本赛道。
|
# 边界口径:AI 基础设施类主题归算力,AI 应用类归本赛道(v0.1 起沿用)。
|
||||||
|
|
||||||
- name: 低空经济
|
- name: 低空经济
|
||||||
status: confirmed
|
status: confirmed
|
||||||
|
|
@ -128,12 +132,12 @@ tracks:
|
||||||
kg_chains: [低空经济] # 24
|
kg_chains: [低空经济] # 24
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): 大飞机(11) 通用航空口径是否入。
|
# 已拍(07-31): 大飞机(11) 不收(同商业航空航天条目的拍板)。
|
||||||
|
|
||||||
# ---- 以下 candidate:G1 初判覆盖非空,进不进清单等 §9-2 拍板 ----
|
# ---- 以下四个赛道 07-31 由 candidate 转正(用户批"全按建议")----
|
||||||
|
|
||||||
- name: 脑机接口
|
- name: 脑机接口
|
||||||
status: candidate
|
status: confirmed
|
||||||
aliases: []
|
aliases: []
|
||||||
kg_themes:
|
kg_themes:
|
||||||
- 脑机接口 # 51
|
- 脑机接口 # 51
|
||||||
|
|
@ -142,7 +146,7 @@ tracks:
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
|
|
||||||
- name: 氢能
|
- name: 氢能
|
||||||
status: candidate
|
status: confirmed
|
||||||
aliases: []
|
aliases: []
|
||||||
kg_themes:
|
kg_themes:
|
||||||
- 氢能 # 68
|
- 氢能 # 68
|
||||||
|
|
@ -150,10 +154,10 @@ tracks:
|
||||||
kg_chains: [SOFC] # 20
|
kg_chains: [SOFC] # 20
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# TODO(拍板): 绿色甲醇(31 主题/18 链) 绿色燃料口径是否并入。
|
# 已拍(07-31): 绿色甲醇(31 主题/18 链) 不收——保持氢能纯度,将来可单列绿色燃料。
|
||||||
|
|
||||||
- name: 深海科技
|
- name: 深海科技
|
||||||
status: candidate
|
status: confirmed
|
||||||
aliases: []
|
aliases: []
|
||||||
kg_themes:
|
kg_themes:
|
||||||
- 深海科技 # 20
|
- 深海科技 # 20
|
||||||
|
|
@ -164,7 +168,7 @@ tracks:
|
||||||
# LIKE '%深海%' 对 chain 和 segment_name 各查一次再补。
|
# LIKE '%深海%' 对 chain 和 segment_name 各查一次再补。
|
||||||
|
|
||||||
- name: 固态电池
|
- name: 固态电池
|
||||||
status: candidate
|
status: confirmed
|
||||||
aliases: []
|
aliases: []
|
||||||
kg_themes:
|
kg_themes:
|
||||||
- 固态电池 # 93
|
- 固态电池 # 93
|
||||||
|
|
@ -172,8 +176,8 @@ tracks:
|
||||||
kg_chains: [固态电池] # 41
|
kg_chains: [固态电池] # 41
|
||||||
kg_segments: []
|
kg_segments: []
|
||||||
kg_concepts: []
|
kg_concepts: []
|
||||||
# 备注:是否算"十五五前沿"待 §9-2;若不算独立赛道可并入新能源体系不进 C。
|
# 已拍(07-31): 作为独立赛道转正;锂电池(63)/锂电(52)/锂离子电池(14)/
|
||||||
# TODO(拍板): 锂电池(63)/锂电(52)/锂离子电池(14)/电池(38) 等链名太宽,均未收。
|
# 电池(38) 等链名太宽,均不收——保持固态电池纯度。
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 排除清单
|
# 排除清单
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# 每日选股计划 · 下游对接说明
|
||||||
|
|
||||||
|
(akg-factor-bridge 产出,2026-07-31 版)
|
||||||
|
|
||||||
|
## 系统逻辑,一句话
|
||||||
|
|
||||||
|
基于 A 股产业知识图谱的"景气度漏斗":每个交易日早晨,从约 2400 只覆盖股中找出
|
||||||
|
**被前一交易日异动板块沿产业链指向、自身还没动、热度还低、券商预期仍有空间**的
|
||||||
|
标的,按置信度分档,给出全局可排序的名单。
|
||||||
|
|
||||||
|
核心信号是"传导":某板块昨日异动,知识图谱知道它的上下游与供应链,顺着链找到
|
||||||
|
"下一个"还没动的环节及其成员。热度与估值只做组内排序,不喧宾夺主。
|
||||||
|
|
||||||
|
## 节奏与获取
|
||||||
|
|
||||||
|
- 生成时间:每个交易日早晨约 07:15 前就绪(周二至周六晨跑;周一盘前读周六那期)。
|
||||||
|
- 数据基准:上一交易日收盘的行情与资金热度、截至当日凌晨的券商一致预期与公告图谱。
|
||||||
|
- 获取(桥机 8300 端口,局域网 HTTP):
|
||||||
|
- `GET /plan` —— 最新一期,JSON(main / observe / changes 三段);
|
||||||
|
- `GET /plan?format=md` —— 同内容 Markdown,浏览器可读;
|
||||||
|
- `GET /plan?date=YYYY-MM-DD` —— 指定期。**日期 = 数据日(上一交易日)**,
|
||||||
|
例如周五早晨这期的日期是周四;查不到返回 404;
|
||||||
|
- `GET /plan/dates` —— 可取日期列表;`GET /health` —— 存活探测。
|
||||||
|
- 展示默认:主榜前 20、观察档前 10、每个传导主题最多 5 只(防单板块刷屏)。
|
||||||
|
JSON 的 counts 字段给全量规模;要更长名单可带参数(top / obs_top / theme_cap),
|
||||||
|
或直接读平台因子表 t_factor_akg_score / t_factor_akg_gate 取全量。
|
||||||
|
|
||||||
|
## 三档结构与数量预期
|
||||||
|
|
||||||
|
| 档位 | 判据 | 分数区间 | 典型规模 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 主榜 | 有券商一致预期,且目标价不低于现价 | 190~250 | 950~1000 只 |
|
||||||
|
| 观察档 | 无券商覆盖,但在图谱传导链上(无估值锚,置信度低) | 90~110 | 80~110 只 |
|
||||||
|
| 不采纳 | 两类锚都没有,或目标价低于现价 | 不出行 | 其余 |
|
||||||
|
|
||||||
|
- 分数全局可排序,150 为主榜与观察档的天然分界。
|
||||||
|
- 主榜内部 = 200 + 传导档位×20 + 组内分:强传导(多个异动源指向、强度高)>
|
||||||
|
弱传导 > 无传导,同档内按"还没热、还便宜"排。
|
||||||
|
- **强传导档的规模随市场结构波动**:取决于前一交易日有几个板块异动并有效传导,
|
||||||
|
典型 1~5 个主题、合计几只到几十只;无板块传导的日子主榜靠"冷+便宜"排序。
|
||||||
|
- 每只候选带证据:传导主题(几个异动源、同板块已动比例)、热度(0~1)、预期空间。
|
||||||
|
- changes 段(升降档):覆盖新增、估值闸翻转、传导链进出引起的档位变动,每日
|
||||||
|
几十只量级——"首次覆盖 / 新进传导链"本身是有信息量的事件。
|
||||||
|
|
||||||
|
## 口径须知
|
||||||
|
|
||||||
|
- **预期空间的绝对值偏高**(一致预期取 90 天内全部研报的简单平均,且券商研报
|
||||||
|
报喜不报忧),请当**相对排序**用,不要当收益预测读。
|
||||||
|
- 热度是生态内日频资金热度分(0~1),次日到达,系统已按可得的最新日对齐。
|
||||||
|
- **ST / *ST / 退市族默认已被重大风险闸挡在名单之外**(按证券简称识别,系统
|
||||||
|
参数可关——关了才会出现)。
|
||||||
|
- 升降档每条带原因标注:首次覆盖 / 估值转正 / 新进传导链 / 估值转负 /
|
||||||
|
离开传导链等(启发式归类,精确审计以档位表与因子表为准)。
|
||||||
|
- 赛道硬门槛(十五五前沿产业链:核聚变、商业航天、通信、算力、人工智能、
|
||||||
|
低空经济等)已建成、暂未启用。启用后主榜将收窄为"赛道内"子集(按当前
|
||||||
|
测算约为现规模的三分之一),届时另行通知。
|
||||||
|
- 传导史自 2026-07 起为纯实时积累,无回填历史;系统各环节的判定依据均可
|
||||||
|
审计(每只候选可回溯到具体的图谱边、异动台账与研报行)。
|
||||||
47
factors.py
47
factors.py
|
|
@ -347,6 +347,38 @@ def _panel(ds: str) -> pd.DataFrame:
|
||||||
return pd.concat([up, ht, tr], axis=1)
|
return pd.concat([up, ht, tr], axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
_RISK_PREFIXES = ("*ST", "ST", "SST", "S*ST", "退市")
|
||||||
|
|
||||||
|
|
||||||
|
def _risk_set() -> set:
|
||||||
|
"""重大风险名单(07-31 拍板:默认杜绝):按证券简称识别 ST/*ST/退市族。
|
||||||
|
|
||||||
|
名称来自 industry_pools 成员表;开关 config.EXCLUDE_RISK_NAMES(默认开)。
|
||||||
|
加载失败只告警不中止——宁可当天不过滤,也不让计划断产。"""
|
||||||
|
if not config.EXCLUDE_RISK_NAMES:
|
||||||
|
return set()
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
pools = db.read_pg("SELECT members FROM industry_pools")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" ⚠️ 风险名单加载失败(本轮不过滤): {e!r}")
|
||||||
|
return set()
|
||||||
|
out = set()
|
||||||
|
for _, r in pools.iterrows():
|
||||||
|
ms = r["members"]
|
||||||
|
if isinstance(ms, str):
|
||||||
|
ms = json.loads(ms)
|
||||||
|
for m in ms or []:
|
||||||
|
ts = (m or {}).get("ts_code")
|
||||||
|
name = str((m or {}).get("name") or "").replace(" ", "")
|
||||||
|
if ts and (name.startswith(_RISK_PREFIXES) or name.endswith("退")):
|
||||||
|
out.add(common.to_prefix(ts))
|
||||||
|
if out:
|
||||||
|
print(f" (重大风险闸: {len(out)} 只 ST/退市族挡在档位外,"
|
||||||
|
f"EXCLUDE_RISK_NAMES=0 可关)")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _track_set():
|
def _track_set():
|
||||||
"""已转正赛道的成员集合(前缀码)。C 闸未启用时返回 None。"""
|
"""已转正赛道的成员集合(前缀码)。C 闸未启用时返回 None。"""
|
||||||
if not config.ENABLE_TRACK_GATE:
|
if not config.ENABLE_TRACK_GATE:
|
||||||
|
|
@ -360,11 +392,12 @@ def _track_set():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _gate_of(p: pd.DataFrame, track_set) -> pd.Series:
|
def _gate_of(p: pd.DataFrame, track_set, risk_set=None) -> pd.Series:
|
||||||
"""三档(07-30 拍板):
|
"""三档(07-30 拍板):
|
||||||
2 = 主榜:有业绩锚(券商覆盖且 upside>=0);C 闸开启后再交赛道成员;
|
2 = 主榜:有业绩锚(券商覆盖且 upside>=0);C 闸开启后再交赛道成员;
|
||||||
1 = 观察档:无业绩锚但有产业链锚(当前=在传导链上;C 转正后并入赛道成员);
|
1 = 观察档:无业绩锚但有产业链锚(当前=在传导链上;C 转正后并入赛道成员);
|
||||||
0 = 不采纳:两锚皆无,或有覆盖但 upside<0(贵了不买是绝对下限)。"""
|
0 = 不采纳:两锚皆无,有覆盖但 upside<0(贵了不买是绝对下限),
|
||||||
|
或命中重大风险闸(ST/退市族,07-31 拍板默认杜绝)。"""
|
||||||
covered = p["upside"].notna()
|
covered = p["upside"].notna()
|
||||||
chain = p["transmission"].fillna(0.0) > 0
|
chain = p["transmission"].fillna(0.0) > 0
|
||||||
if track_set:
|
if track_set:
|
||||||
|
|
@ -376,6 +409,8 @@ def _gate_of(p: pd.DataFrame, track_set) -> pd.Series:
|
||||||
main = main & in_track
|
main = main & in_track
|
||||||
g[main] = 2.0
|
g[main] = 2.0
|
||||||
g[(~covered) & chain] = 1.0
|
g[(~covered) & chain] = 1.0
|
||||||
|
if risk_set:
|
||||||
|
g[p.index.isin(risk_set)] = 0.0
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -391,12 +426,13 @@ def build_gate(start, end):
|
||||||
"""akg_gate ∈ {0,1,2}:全池出行——让平台看得见门槛本身,不只是池内排序。
|
"""akg_gate ∈ {0,1,2}:全池出行——让平台看得见门槛本身,不只是池内排序。
|
||||||
history 模式逐日重算三路子因子,区间大会慢;建议 daily / 短区间。"""
|
history 模式逐日重算三路子因子,区间大会慢;建议 daily / 短区间。"""
|
||||||
ts = _track_set()
|
ts = _track_set()
|
||||||
|
rs = _risk_set()
|
||||||
rows = []
|
rows = []
|
||||||
for ds in _days_of(start, end):
|
for ds in _days_of(start, end):
|
||||||
p = _panel(ds)
|
p = _panel(ds)
|
||||||
if p.empty:
|
if p.empty:
|
||||||
continue
|
continue
|
||||||
g = _gate_of(p, ts)
|
g = _gate_of(p, ts, rs)
|
||||||
rows.append(pd.DataFrame({"trade_date": ds, "stock_code": g.index,
|
rows.append(pd.DataFrame({"trade_date": ds, "stock_code": g.index,
|
||||||
"factor_value": g.values}))
|
"factor_value": g.values}))
|
||||||
return pd.concat(rows, ignore_index=True) if rows else _EMPTY
|
return pd.concat(rows, ignore_index=True) if rows else _EMPTY
|
||||||
|
|
@ -405,19 +441,20 @@ def build_gate(start, end):
|
||||||
def build_score(start, end):
|
def build_score(start, end):
|
||||||
"""akg_score:先档后分,仅 gate>0 出行,数值直接可排序。
|
"""akg_score:先档后分,仅 gate>0 出行,数值直接可排序。
|
||||||
|
|
||||||
主榜 = 200 + 传导档位×10 + 组内分:传导票按 log 传导分的组内中位数分成
|
主榜 = 200 + 传导档位×20 + 组内分:传导票按 log 传导分的组内中位数分成
|
||||||
强(2)/弱(1)两档,无传导 0 档;组内分 = 0.6·z(−热度) + 0.4·z(upside),
|
强(2)/弱(1)两档,无传导 0 档;组内分 = 0.6·z(−热度) + 0.4·z(upside),
|
||||||
即"还没热、还便宜"。观察档 = 100 + 0.6·z(传导) + 0.4·z(−热度)——无估值锚,
|
即"还没热、还便宜"。观察档 = 100 + 0.6·z(传导) + 0.4·z(−热度)——无估值锚,
|
||||||
低置信。**档位步长 20、组内分夹 ±9.9**:组内分全幅 19.8 小于步长,
|
低置信。**档位步长 20、组内分夹 ±9.9**:组内分全幅 19.8 小于步长,
|
||||||
先档后分的词典序才真正成立(步长 10 会档间重叠,07-30 交付当天修正)。
|
先档后分的词典序才真正成立(步长 10 会档间重叠,07-30 交付当天修正)。
|
||||||
z 都在各自档内算;缺失处置同设计 §3.4:传导缺=0,热度缺=档内中位数。"""
|
z 都在各自档内算;缺失处置同设计 §3.4:传导缺=0,热度缺=档内中位数。"""
|
||||||
ts = _track_set()
|
ts = _track_set()
|
||||||
|
rs = _risk_set()
|
||||||
out = []
|
out = []
|
||||||
for ds in _days_of(start, end):
|
for ds in _days_of(start, end):
|
||||||
p = _panel(ds)
|
p = _panel(ds)
|
||||||
if p.empty:
|
if p.empty:
|
||||||
continue
|
continue
|
||||||
g = _gate_of(p, ts)
|
g = _gate_of(p, ts, rs)
|
||||||
|
|
||||||
P = p[g == 2.0].copy() # ---- 主榜
|
P = p[g == 2.0].copy() # ---- 主榜
|
||||||
if not P.empty:
|
if not P.empty:
|
||||||
|
|
|
||||||
39
plan.py
39
plan.py
|
|
@ -158,9 +158,44 @@ def collect(date: str | None = None, top: int = 20, obs_top: int = 10,
|
||||||
up_df = both[both["cur"] > both["prev"]].sort_values("cur", ascending=False)
|
up_df = both[both["cur"] > both["prev"]].sort_values("cur", ascending=False)
|
||||||
down_df = both[both["cur"] < both["prev"]].sort_values("prev", ascending=False)
|
down_df = both[both["cur"] < both["prev"]].sort_values("prev", ascending=False)
|
||||||
|
|
||||||
|
# ---- 升降原因(07-31 加):区分首次覆盖 / 估值转正 / 新进传导链等。
|
||||||
|
# 依据前一日的 upside / 传导因子表;表空时现算兜底。原因是启发式归类
|
||||||
|
# (取最主要的一条),精确审计以档位表与因子表为准。
|
||||||
|
prev_up = _factor("t_factor_akg_upside", prev_ds)
|
||||||
|
if prev_up.empty:
|
||||||
|
try:
|
||||||
|
import factors
|
||||||
|
dfu = factors.build_upside(prev_ds, prev_ds)
|
||||||
|
if dfu is not None and not dfu.empty:
|
||||||
|
x = dfu.copy()
|
||||||
|
x["k"] = x["stock_code"].map(common.to_prefix)
|
||||||
|
prev_up = x.groupby("k")["factor_value"].max().astype(float)
|
||||||
|
except Exception: # noqa: BLE001 —— 兜底失败则原因退化为通用文案
|
||||||
|
pass
|
||||||
|
prev_tr = _factor("t_factor_akg_transmission", prev_ds)
|
||||||
|
|
||||||
|
def _why(k: str, pg: float, cg: float) -> str:
|
||||||
|
if cg == 2.0: # 升入主榜
|
||||||
|
pu = _val(prev_up, k)
|
||||||
|
if pu is None:
|
||||||
|
return "首次覆盖"
|
||||||
|
return "估值转正" if pu < 0 else "重获资格"
|
||||||
|
if cg == 1.0: # 升入观察档
|
||||||
|
pt = _val(prev_tr, k)
|
||||||
|
return "新进传导链" if pt is None or pt <= 0 else "档位调整"
|
||||||
|
if pg == 2.0: # 从主榜降出
|
||||||
|
cu = _val(upside, k)
|
||||||
|
if cu is None:
|
||||||
|
return "覆盖脱落"
|
||||||
|
return "估值转负" if cu < 0 else "风险闸/档位调整"
|
||||||
|
if pg == 1.0: # 从观察档降出
|
||||||
|
return "离开传导链" if k not in ev else "风险闸/档位调整"
|
||||||
|
return "出入面板"
|
||||||
|
|
||||||
def _mv(d: pd.DataFrame):
|
def _mv(d: pd.DataFrame):
|
||||||
return [{"code": k, "name": names.get(k),
|
return [{"code": k, "name": names.get(k),
|
||||||
"from": lab.get(r["prev"], "?"), "to": lab.get(r["cur"], "?")}
|
"from": lab.get(r["prev"], "?"), "to": lab.get(r["cur"], "?"),
|
||||||
|
"reason": _why(k, r["prev"], r["cur"])}
|
||||||
for k, r in d.iterrows()]
|
for k, r in d.iterrows()]
|
||||||
|
|
||||||
changes = {"base_date": prev_ds,
|
changes = {"base_date": prev_ds,
|
||||||
|
|
@ -245,6 +280,7 @@ def render_md(d: dict) -> str:
|
||||||
L.append("")
|
L.append("")
|
||||||
L.append("**升档**:")
|
L.append("**升档**:")
|
||||||
L += [f"- {m['code']} {m['name'] or ''}:{m['from']} → {m['to']}"
|
L += [f"- {m['code']} {m['name'] or ''}:{m['from']} → {m['to']}"
|
||||||
|
f"({m.get('reason', '—')})"
|
||||||
for m in ch["upgrades"]]
|
for m in ch["upgrades"]]
|
||||||
if ch["upgrades_total"] > len(ch["upgrades"]):
|
if ch["upgrades_total"] > len(ch["upgrades"]):
|
||||||
L.append(f"- ……共 {ch['upgrades_total']} 只,其余见档位表")
|
L.append(f"- ……共 {ch['upgrades_total']} 只,其余见档位表")
|
||||||
|
|
@ -252,6 +288,7 @@ def render_md(d: dict) -> str:
|
||||||
L.append("")
|
L.append("")
|
||||||
L.append("**降档**:")
|
L.append("**降档**:")
|
||||||
L += [f"- {m['code']} {m['name'] or ''}:{m['from']} → {m['to']}"
|
L += [f"- {m['code']} {m['name'] or ''}:{m['from']} → {m['to']}"
|
||||||
|
f"({m.get('reason', '—')})"
|
||||||
for m in ch["downgrades"]]
|
for m in ch["downgrades"]]
|
||||||
if ch["downgrades_total"] > len(ch["downgrades"]):
|
if ch["downgrades_total"] > len(ch["downgrades"]):
|
||||||
L.append(f"- ……共 {ch['downgrades_total']} 只,其余见档位表")
|
L.append(f"- ……共 {ch['downgrades_total']} 只,其余见档位表")
|
||||||
|
|
|
||||||
6
run.py
6
run.py
|
|
@ -206,7 +206,11 @@ def main():
|
||||||
print(f"\nconfirmed 成员表快照: {out}"
|
print(f"\nconfirmed 成员表快照: {out}"
|
||||||
f"({df['ts_code'].nunique()} 只,{len(df)} 行 股票×赛道)")
|
f"({df['ts_code'].nunique()} 只,{len(df)} 行 股票×赛道)")
|
||||||
if missing:
|
if missing:
|
||||||
print(f"⚠️ {len(missing)} 个主题在 industry_pools 里没找到(见体检表)")
|
print(f"⚠️ {len(missing)} 个映射键未命中(明细见体检表各行)")
|
||||||
|
try:
|
||||||
|
tracks.gate_simulation()
|
||||||
|
except Exception as e: # noqa: BLE001 —— 演算失败不影响体检本体
|
||||||
|
print(f"(赛道闸演算失败: {e!r})")
|
||||||
elif a.cmd == "build":
|
elif a.cmd == "build":
|
||||||
cmd_build(a.factor, a.mode, a.start, a.end, a.date, do_freeze=not a.no_freeze)
|
cmd_build(a.factor, a.mode, a.start, a.end, a.date, do_freeze=not a.no_freeze)
|
||||||
|
|
||||||
|
|
|
||||||
50
tracks.py
50
tracks.py
|
|
@ -166,3 +166,53 @@ def coverage_report(path: str | None = None) -> pd.DataFrame:
|
||||||
n_conf_graph = conf[conf["source_rule"] != "pool_theme"]["ts_code"].nunique()
|
n_conf_graph = conf[conf["source_rule"] != "pool_theme"]["ts_code"].nunique()
|
||||||
print(f" —— confirmed 合计(去重): {n_conf} 只,其中带图谱锚 {n_conf_graph} 只")
|
print(f" —— confirmed 合计(去重): {n_conf} 只,其中带图谱锚 {n_conf_graph} 只")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def gate_simulation(path: str | None = None) -> None:
|
||||||
|
"""赛道闸演算(只读):若 ENABLE_TRACK_GATE 置 1,主榜/观察档变成什么规模。
|
||||||
|
|
||||||
|
口径与开闸后的 factors._gate_of 一致:
|
||||||
|
主榜' = 现主榜 ∩ 赛道成员;
|
||||||
|
观察档' = 现观察档 ∪(无券商预期 ∧ 在赛道 ∧ 现为不采纳 ∧ 非风险股)。
|
||||||
|
读最新档位 / upside 因子表与 confirmed 成员表,不写任何东西。"""
|
||||||
|
import common
|
||||||
|
|
||||||
|
d = db.read_mysql("factor", "SELECT MAX(trade_date) d FROM t_factor_akg_gate")
|
||||||
|
v = None if d.empty else d.iloc[0, 0]
|
||||||
|
if v is None or pd.isna(v):
|
||||||
|
print("(尚无档位表,跳过赛道闸演算)")
|
||||||
|
return
|
||||||
|
ds = pd.Timestamp(v).date().isoformat()
|
||||||
|
g = db.read_mysql("factor", "SELECT stock_code, factor_value "
|
||||||
|
"FROM t_factor_akg_gate WHERE trade_date = %s", (ds,))
|
||||||
|
up = db.read_mysql("factor", "SELECT stock_code FROM t_factor_akg_upside "
|
||||||
|
"WHERE trade_date = %s", (ds,))
|
||||||
|
gate = g.set_index("stock_code")["factor_value"].astype(float)
|
||||||
|
covered = set(up["stock_code"]) if not up.empty else set()
|
||||||
|
df, _missing = resolve_members(only_confirmed=True, path=path)
|
||||||
|
if df.empty:
|
||||||
|
print("(赛道成员表为空,跳过演算)")
|
||||||
|
return
|
||||||
|
by_track = {t: {common.to_prefix(x) for x in sub["ts_code"]}
|
||||||
|
for t, sub in df.groupby("track")}
|
||||||
|
tset = set().union(*by_track.values())
|
||||||
|
try:
|
||||||
|
import factors
|
||||||
|
risk = factors._risk_set() # noqa: SLF001 —— 同仓自用
|
||||||
|
except Exception: # noqa: BLE001 —— 风险名单拿不到时演算照做
|
||||||
|
risk = set()
|
||||||
|
main_now = set(gate[gate == 2.0].index)
|
||||||
|
obs_now = set(gate[gate == 1.0].index)
|
||||||
|
main_after = main_now & tset
|
||||||
|
obs_extra = {k for k in tset
|
||||||
|
if k not in covered and gate.get(k) == 0.0 and k not in risk}
|
||||||
|
obs_after = obs_now | obs_extra
|
||||||
|
print(f"\n赛道闸演算(截面 {ds},假想 ENABLE_TRACK_GATE=1):")
|
||||||
|
print(f" 主榜: {len(main_now)} → {len(main_after)}"
|
||||||
|
f"(保留 {len(main_after) / max(1, len(main_now)):.0%})")
|
||||||
|
print(f" 观察档: {len(obs_now)} → {len(obs_after)}"
|
||||||
|
f"(并入赛道内无覆盖股 +{len(obs_extra)})")
|
||||||
|
parts = [f"{t} {len(main_now & s)}"
|
||||||
|
for t, s in sorted(by_track.items(),
|
||||||
|
key=lambda kv: -len(main_now & kv[1]))]
|
||||||
|
print(f" 逐赛道主榜贡献(跨赛道有重叠): {' | '.join(parts)}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue