akg-factor-bridge/factors.py

166 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""四路子因子构造。输入日期区间 [start, end]YYYY-MM-DD输出
DataFrame[trade_date, stock_code, factor_value],全部限制在 KG 覆盖池 universe 内。
stock_code 输出形态不限common.write_factor 统一转前缀式。
建模参数EVENT_POLARITY / *_HALF_LIFE / *_WINDOW是**因子决策**,见设计文档
§3.3 与 §8-3此处取草案默认待用户确认后调。方向(direction)统一在合成配置里声明,
子因子表只存原始值——但 event 天然带符号,故此处保留极性。
"""
import numpy as np
import pandas as pd
import config
import common
import db
FACTORS = {
"akg_upside": "t_factor_akg_upside",
"akg_heat": "t_factor_akg_heat",
"akg_event": "t_factor_akg_event",
"akg_transmission": "t_factor_akg_transmission",
}
# ---- 事件极性草案§3.3,待 §8-3 确认)----
EVENT_POLARITY = {
"股份回购": 1.0, "重大合同中标": 1.0, "股权激励授予": 0.5,
"诉讼仲裁": -1.0, "行政处罚": -1.0, "股权质押": -0.5,
"发行上市": 0.0, "并购交割": 0.0, "other": 0.0,
# 增减持 / 业绩预告:符号取决于 direction下 EVENT_DIR
"增减持": 0.0, "业绩预告": 0.0,
}
EVENT_DIR = {"预增": 1.0, "预减": -1.0, "增持": 1.0, "减持": -1.0}
EVENT_HALF_LIFE = 10 # 交易日
EVENT_WINDOW = 60 # 交易日(超窗不计)
_EMPTY = pd.DataFrame(columns=["trade_date", "stock_code", "factor_value"])
# ---------------------------------------------------------------- 热度
def build_heat(start, end):
"""热度 = stock_fund_heat_scores 最新批次 score0~1。stock_code 已前缀式。"""
uni = {common.to_prefix(x) for x in common.load_universe()}
df = db.read_mysql("heat",
"""SELECT s.trade_date, s.stock_code, s.score AS factor_value
FROM stock_fund_heat_scores s
JOIN (SELECT trade_date, MAX(batch_no) bn FROM stock_fund_heat_scores
WHERE trade_date BETWEEN %s AND %s GROUP BY trade_date) m
ON m.trade_date = s.trade_date AND m.bn = s.batch_no""",
(start, end))
if df.empty:
return _EMPTY
df["stock_code"] = df["stock_code"].astype(str).str.strip()
df["factor_value"] = pd.to_numeric(df["factor_value"], errors="coerce")
df = df[df["stock_code"].isin(uni)]
return df[["trade_date", "stock_code", "factor_value"]]
# ---------------------------------------------------------------- 预期空间
def build_upside(start, end):
"""upside = 一致预期目标价中枢 / 当日现价 1as-of现价日取 asof<=当日最新一致预期)。
现价来自平台 gp_day_data代码列 config.PRICE_CODE_COL待实机核实"""
uni = common.load_universe()
cons = db.read_pg(
"SELECT ts_code, asof_date, target_mid_avg FROM v_factor_consensus WHERE asof_date <= %s",
(end,))
cons = cons[cons["ts_code"].isin(uni)].copy()
if cons.empty:
return _EMPTY
col = config.PRICE_CODE_COL
price = db.read_mysql("price",
f"SELECT `timestamp` AS trade_date, `{col}` AS ts_code, close "
f"FROM gp_day_data WHERE `timestamp` BETWEEN %s AND %s", (start, end))
if price.empty:
return _EMPTY
price["close"] = pd.to_numeric(price["close"], errors="coerce")
# 归一到前缀式两边对齐gp_day_data 代码形态不定 → 都折前缀式后 join
price["k"] = price["ts_code"].map(common.to_prefix)
price = price[(price["close"] > 0)].dropna(subset=["close"])
cons["k"] = cons["ts_code"].map(common.to_prefix)
cons = cons[cons["k"].isin(set(price["k"]))]
if cons.empty:
return _EMPTY
cons["asof_date"] = pd.to_datetime(cons["asof_date"])
price["trade_date"] = pd.to_datetime(price["trade_date"])
out = []
cons_sorted = cons.sort_values("asof_date")
for k, pg in price.groupby("k"):
cg = cons_sorted[cons_sorted["k"] == k]
if cg.empty:
continue
m = pd.merge_asof(pg.sort_values("trade_date"),
cg[["asof_date", "target_mid_avg"]],
left_on="trade_date", right_on="asof_date", direction="backward")
m = m.dropna(subset=["target_mid_avg"])
if m.empty:
continue
m["factor_value"] = m["target_mid_avg"].astype(float) / m["close"] - 1.0
m["stock_code"] = k
out.append(m[["trade_date", "stock_code", "factor_value"]])
return pd.concat(out) if out else _EMPTY
# ---------------------------------------------------------------- 事件
def _polarity(event_type, direction):
d = (direction or "").strip()
if d in EVENT_DIR:
return EVENT_DIR[d]
return EVENT_POLARITY.get(event_type, 0.0)
def build_event(start, end):
"""事件分 = Σ 近窗口内事件 极性 × 时间衰减exp(-交易日龄·ln2/半衰期))。
ts_code 取文档锚v_factor_events 已解析)。无事件的股当天不出行(= 缺 → 合成侧填 0"""
uni = common.load_universe()
look = (pd.Timestamp(start) - pd.Timedelta(days=EVENT_WINDOW * 2)).date()
ev = db.read_pg(
"SELECT ts_code, disclosure_date, event_type, direction "
"FROM v_factor_events WHERE disclosure_date BETWEEN %s AND %s", (look, end))
ev = ev[ev["ts_code"].isin(uni)].copy()
if ev.empty:
return _EMPTY
ev["pol"] = [_polarity(t, d) for t, d in zip(ev["event_type"], ev["direction"])]
ev = ev[ev["pol"] != 0.0]
if ev.empty:
return _EMPTY
cal = common.trading_days(start, end)
if not cal:
return _EMPTY
cal = pd.DatetimeIndex(cal)
decay = np.log(2) / EVENT_HALF_LIFE
rows = []
for ts, g in ev.groupby("ts_code"):
disc = pd.to_datetime(g["disclosure_date"]).values.astype("datetime64[ns]")
pol = g["pol"].to_numpy(dtype=float)
for d in cal:
# 自然日龄 → 交易日龄近似 ×(5/7)v1 近似,见 README 待优化项)
age_td = ((d.value - disc.astype("int64")) / 86_400e9) * (5.0 / 7.0)
mask = (age_td >= 0) & (age_td <= EVENT_WINDOW)
if not mask.any():
continue
val = float((pol[mask] * np.exp(-age_td[mask] * decay)).sum())
if val != 0.0:
rows.append((d.date(), ts, val))
return pd.DataFrame(rows, columns=["trade_date", "stock_code", "factor_value"]) if rows else _EMPTY
# ---------------------------------------------------------------- 传导
def build_transmission(start, end):
"""传导分 = 指向该股所在环节的路径数 ×1 已动比例);同股同日多候选取最大。"""
uni = common.load_universe()
tr = db.read_pg(
"SELECT scan_date, ts_code, n_paths, moved_ratio "
"FROM v_factor_transmission WHERE scan_date BETWEEN %s AND %s", (start, end))
tr = tr[tr["ts_code"].isin(uni)].copy()
if tr.empty:
return _EMPTY
tr["factor_value"] = (tr["n_paths"].astype(float)
* (1.0 - pd.to_numeric(tr["moved_ratio"], errors="coerce").fillna(0.0)))
g = (tr.groupby(["scan_date", "ts_code"])["factor_value"].max().reset_index()
.rename(columns={"scan_date": "trade_date", "ts_code": "stock_code"}))
return g[["trade_date", "stock_code", "factor_value"]]
BUILDERS = {
"akg_upside": build_upside, "akg_heat": build_heat,
"akg_event": build_event, "akg_transmission": build_transmission,
}